@michengai/dsh-codex-ui 0.2.88 → 0.2.90

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/CHANGELOG.md CHANGED
@@ -4,6 +4,45 @@
4
4
 
5
5
  This changelog records recent releases of DSH Codex UI and its one-click installer. Earlier changes remain available in the [Git history](https://github.com/MichengAI/dsh-codex-ui/commits/main).
6
6
 
7
+ ## 0.2.90 — 2026-08-27
8
+
9
+ Companion release: `@michengai/dsh-codex-suite-installer@0.1.7`.
10
+
11
+ ### Changed
12
+
13
+ - Matched the expandable sidebar to the installed Codex desktop client: 275px default width, 240px minimum, and 520px maximum, capped by available viewport width.
14
+ - Mapped the DSH host's narrower persisted drag range to the Codex visual range while preserving the 275px default anchor.
15
+
16
+ ### Fixed
17
+
18
+ - Replaced the previous coordinate-based collapse gesture with Codex behavior: dragging below 240px collapses the sidebar.
19
+ - Prevented the DOM width adapter from reapplying its own mapped output during host drag and rerender cycles.
20
+
21
+ ## 0.2.89 — 2026-08-27
22
+
23
+ Companion release: `@michengai/dsh-codex-suite-installer@0.1.6`.
24
+
25
+ ### Added
26
+
27
+ - Persisted workspace, channel, and scheduled-task folder expansion independently across refreshes with versioned, failure-safe browser storage.
28
+ - Added scheduled-task list/overview switching, direct task-settings navigation, and group archive actions.
29
+ - Added Node.js 22/24 CI for pushes and pull requests, and made the tag Release workflow run the full test suite before creating a release.
30
+
31
+ ### Fixed
32
+
33
+ - Stopped classifying ordinary conversations as scheduled tasks from an editable timestamp-shaped title; automation ownership now requires the stable session ID prefix.
34
+ - Made group archive continue after individual failures, clean local pin/unread state only for successful items, and leave failures selected for retry.
35
+ - Validated scheduled-task settings requests at runtime, guarded browser storage access, and extended lazy Settings navigation to four seconds.
36
+ - Restricted the Windows Explorer Host endpoint to exact registered workspace roots, rejecting drive roots, UNC paths, relative paths, children, and unrelated absolute paths.
37
+ - Reduced compatibility observer work by filtering unrelated conversation, permission, and Settings DOM mutations before scheduling scans.
38
+
39
+ ### Release process
40
+
41
+ - Aligned the root package, lightweight installer, private legacy Suite snapshot, member pins, and version-contract assertions; the aggregate Suite remains retired.
42
+ - Corrected the installer command and executable name in local-verification help and READMEs, and added a build contract for the client bundle's static runtime modules.
43
+ - Tracked and refreshed the current handoff documentation under `docs/00-交接入口/` while leaving historical documentation ignored.
44
+ - Hardened the Windows installer against shell metacharacters in `DSH_BIN`.
45
+
7
46
  ## 0.2.88 — 2026-08-26
8
47
 
9
48
  Companion release: `@michengai/dsh-codex-suite-installer@0.1.5`.
@@ -4,6 +4,45 @@
4
4
 
5
5
  本日志记录 DSH Codex UI 及其一键安装器的最近发布;更早的变更可查看 [Git 提交历史](https://github.com/MichengAI/dsh-codex-ui/commits/main)。
6
6
 
7
+ ## 0.2.90 — 2026-08-27
8
+
9
+ 配套版本:`@michengai/dsh-codex-suite-installer@0.1.7`。
10
+
11
+ ### 调整
12
+
13
+ - 侧边栏展开宽度按本机 Codex 桌面客户端对齐:默认 275px、最小 240px、最大 520px,并受可用视口宽度限制。
14
+ - 将 DSH 宿主较窄的持久化拖拽范围映射到 Codex 的视觉范围,同时保留 275px 默认锚点。
15
+
16
+ ### 修复
17
+
18
+ - 收缩手势改为 Codex 行为:拖到小于 240px 时收缩,不再使用固定屏幕坐标阈值。
19
+ - 防止 DOM 宽度适配器在宿主拖动和重渲染期间重复换算自身已映射的输出。
20
+
21
+ ## 0.2.89 — 2026-08-27
22
+
23
+ 配套版本:`@michengai/dsh-codex-suite-installer@0.1.6`。
24
+
25
+ ### 新增
26
+
27
+ - 工作区、频道和定时任务文件夹分别持久化展开/折叠状态;存储带版本,损坏或不可用时安全回退。
28
+ - 定时任务侧栏增加列表/概览切换、直接打开任务设置和整组归档。
29
+ - main push 与 PR 在 Node.js 22/24 上运行 CI;tag Release 创建版本前也必须通过完整测试。
30
+
31
+ ### 修复
32
+
33
+ - 不再根据用户可编辑的时间戳格式标题把普通会话误判为定时任务;自动化归属只认稳定的会话 ID 前缀。
34
+ - 整组归档遇到单项失败时继续处理后续会话,只清理成功项的本地置顶/未读状态,并保留失败项供重试。
35
+ - 为定时任务设置请求增加运行时校验,保护浏览器存储访问,并把设置壳慢加载等待时间延长到 4 秒。
36
+ - Windows Explorer Host 端点只允许打开已注册的精确工作区根,拒绝盘符根、UNC、相对路径、子路径与无关绝对路径。
37
+ - 会话、权限和设置 DOM 兼容观察先过滤无关变更,再调度扫描,降低流式输出期间的额外工作。
38
+
39
+ ### 发布流程
40
+
41
+ - 对齐根包、轻量安装器、私有旧 Suite 快照、成员钉版和版本契约断言;聚合 Suite 继续停止发布。
42
+ - 修正本地验证说明中的安装器路径、帮助与错误前缀,并增加客户端 bundle 静态运行时模块的构建契约。
43
+ - 将 `docs/00-交接入口/` 纳入版本控制并按当前实现更新,其他历史文档继续忽略。
44
+ - 拒绝 `DSH_BIN` 中的 Windows shell 元字符,收紧安装器调用边界。
45
+
7
46
  ## 0.2.88 — 2026-08-26
8
47
 
9
48
  配套版本:`@michengai/dsh-codex-suite-installer@0.1.5`。
package/dist/client.js CHANGED
@@ -19,7 +19,8 @@ window.__ModuleLoader__.load({
19
19
  }
20
20
  }
21
21
  let cancelPendingNavigation;
22
- function openSettingsSection(root, label, onMissing) {
22
+ const SETTINGS_NAVIGATION_TIMEOUT_MS = 4e3;
23
+ function openSettingsSection(root, label, onMissing, onSelected) {
23
24
  const labels = typeof label === "string" ? [label] : label;
24
25
  const trigger = root?.querySelector("[aria-haspopup=\"dialog\"]");
25
26
  if (trigger === null || trigger === void 0) {
@@ -47,6 +48,7 @@ window.__ModuleLoader__.load({
47
48
  if (target === void 0) return false;
48
49
  cleanup();
49
50
  target.click();
51
+ onSelected?.();
50
52
  return true;
51
53
  };
52
54
  const schedule = () => {
@@ -61,7 +63,7 @@ window.__ModuleLoader__.load({
61
63
  cleanup();
62
64
  console.warn(`[michengai-codex-ui] 未找到设置分区:${labels.join(" / ")}`);
63
65
  onMissing?.();
64
- }, 1500);
66
+ }, SETTINGS_NAVIGATION_TIMEOUT_MS);
65
67
  observer.observe(document.body, {
66
68
  childList: true,
67
69
  subtree: true
@@ -564,26 +566,33 @@ window.__ModuleLoader__.load({
564
566
  ]
565
567
  });
566
568
  }
567
- function GroupHead({ expanded, title, icon, onToggle }) {
569
+ function GroupHead({ expanded, title, icon, onToggle, actions, menuOpen }) {
568
570
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
569
- className: "dcu-wb-project-head",
571
+ className: `dcu-wb-project-head${menuOpen === true ? " dcu-wb-menu-open" : ""}`,
570
572
  role: "treeitem",
571
573
  "aria-expanded": expanded,
572
574
  tabIndex: 0,
573
575
  onClick: onToggle,
574
576
  onKeyDown: (event) => {
575
- if (event.key === "Enter" || event.key === " ") {
577
+ if (event.target === event.currentTarget && (event.key === "Enter" || event.key === " ")) {
576
578
  event.preventDefault();
577
579
  onToggle();
578
580
  }
579
581
  },
580
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
581
- className: "dcu-wb-folder",
582
- children: icon
583
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
584
- className: "dcu-wb-project-title",
585
- children: title
586
- })]
582
+ children: [
583
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
584
+ className: "dcu-wb-folder",
585
+ children: icon
586
+ }),
587
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
588
+ className: "dcu-wb-project-title",
589
+ children: title
590
+ }),
591
+ actions !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
592
+ className: "dcu-wb-actions",
593
+ children: actions
594
+ })
595
+ ]
587
596
  });
588
597
  }
589
598
  //#endregion
@@ -931,9 +940,14 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
931
940
  style.textContent = CONVERSATION_HEADER_STYLE;
932
941
  doc.head.append(style);
933
942
  }
934
- /** 观察会话顶栏与用户气泡;所有变更按帧合并,避免流式输出时每个 token 都全文档扫描。 */
943
+ const CONVERSATION_DECORATION_SELECTOR = "header, [data-conversation-scroll], [data-chat-flow-kind=\"user\"], [data-pending-steering]";
944
+ function conversationDecorationMutation(records) {
945
+ const relevant = (node) => node instanceof Element && (node.matches(CONVERSATION_DECORATION_SELECTOR) || node.closest("header, [data-chat-flow-kind=\"user\"], [data-pending-steering]") !== null || node.querySelector(CONVERSATION_DECORATION_SELECTOR) !== null);
946
+ return records.some((record) => relevant(record.target) || [...record.addedNodes].some(relevant) || [...record.removedNodes].some(relevant));
947
+ }
948
+ /** 观察会话顶栏与用户气泡;只对相关子树变更按帧合并,流式回答不会触发全文档扫描。 */
935
949
  function observeConversationHeader(doc = document) {
936
- if (doc.head === null) return () => {};
950
+ if (doc.head === null || doc.body === null) return () => {};
937
951
  ensureStyle(doc);
938
952
  ensureUserBubbleStyle(doc);
939
953
  let applying = false;
@@ -965,8 +979,10 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
965
979
  frame = window.requestAnimationFrame(run);
966
980
  };
967
981
  sync();
968
- const observer = new MutationObserver(sync);
969
- observer.observe(doc.documentElement, {
982
+ const observer = new MutationObserver((records) => {
983
+ if (conversationDecorationMutation(records)) sync();
984
+ });
985
+ observer.observe(doc.body, {
970
986
  childList: true,
971
987
  subtree: true
972
988
  });
@@ -1163,9 +1179,11 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1163
1179
  ]
1164
1180
  });
1165
1181
  }
1166
- const AUTOMATION_TITLE_RE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}\s+-\s+/;
1167
- function isScheduleSession(id, title) {
1168
- return id.startsWith("dsh-automation-session-") || AUTOMATION_TITLE_RE.test(title.trim());
1182
+ //#endregion
1183
+ //#region src/client/schedule-sessions.ts
1184
+ const AUTOMATION_SESSION_PREFIX = "dsh-automation-session-";
1185
+ function isScheduleSession(id, _title) {
1186
+ return id.startsWith(AUTOMATION_SESSION_PREFIX);
1169
1187
  }
1170
1188
  function scheduleGroupName(title) {
1171
1189
  const name = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2})\s*-\s*(.+)$/.exec(title.trim())?.[2]?.trim();
@@ -1188,6 +1206,9 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1188
1206
  return [...groups.values()].map((group) => ({
1189
1207
  ...group,
1190
1208
  sessions: [...group.sessions].sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0))
1209
+ })).sort((left, right) => left.label.localeCompare(right.label, "zh-CN", {
1210
+ numeric: true,
1211
+ sensitivity: "base"
1191
1212
  }));
1192
1213
  }
1193
1214
  //#endregion
@@ -1297,6 +1318,40 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1297
1318
  return fallback !== void 0 && fallback.trim() !== "" ? fallback : void 0;
1298
1319
  }
1299
1320
  //#endregion
1321
+ //#region src/client/tree-expansion.ts
1322
+ const WORKSPACE_EXPANSION_STORAGE_KEY = "dsh-codex-ui.workspace-expansion.v1";
1323
+ const CHANNEL_EXPANSION_STORAGE_KEY = "dsh-codex-ui.channel-expansion.v1";
1324
+ const SCHEDULE_EXPANSION_STORAGE_KEY = "dsh-codex-ui.schedule-expansion.v1";
1325
+ const MAX_EXPANSION_ENTRIES = 500;
1326
+ const MAX_EXPANSION_KEY_LENGTH = 512;
1327
+ function browserStorage() {
1328
+ if (typeof window === "undefined") return void 0;
1329
+ try {
1330
+ return window.localStorage;
1331
+ } catch {
1332
+ return;
1333
+ }
1334
+ }
1335
+ function parseTreeExpansionState(value) {
1336
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return {};
1337
+ const entries = Object.entries(value).filter(([key, expanded]) => key.length > 0 && key.length <= MAX_EXPANSION_KEY_LENGTH && typeof expanded === "boolean").slice(0, MAX_EXPANSION_ENTRIES);
1338
+ return Object.fromEntries(entries);
1339
+ }
1340
+ function readTreeExpansionState(storage, key) {
1341
+ if (storage === void 0) return {};
1342
+ try {
1343
+ return parseTreeExpansionState(JSON.parse(storage.getItem(key) ?? "{}"));
1344
+ } catch {
1345
+ return {};
1346
+ }
1347
+ }
1348
+ function writeTreeExpansionState(storage, key, state) {
1349
+ if (storage === void 0) return;
1350
+ try {
1351
+ storage.setItem(key, JSON.stringify(parseTreeExpansionState(state)));
1352
+ } catch {}
1353
+ }
1354
+ //#endregion
1300
1355
  //#region src/client/CodexWorkspaceBrowser.tsx
1301
1356
  const stylesheet$4 = `
1302
1357
  .dcu-wb{--dcu-wb-inset:4px;display:flex;flex:1;min-height:0;flex-direction:column;padding:4px var(--dcu-wb-inset) 8px;color:var(--dcu-sidebar-primary)}
@@ -1342,12 +1397,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1342
1397
  const typographyStyles = `.dcu-wb{font:14px/20px var(--dcu-font,var(--dsw-font-family))}.dcu-wb-section-label{color:var(--dcu-sidebar-tertiary);font-size:13px;font-weight:400;letter-spacing:.02em}.dcu-wb-project-title{font-size:14px;line-height:20px;font-weight:400;color:var(--dcu-sidebar-primary)}.dcu-wb-session-title{font-size:14px;line-height:20px;font-weight:400;color:var(--dcu-sidebar-secondary)}.dcu-wb-session.dcu-wb-selected .dcu-wb-session-title,.dcu-wb-session:hover .dcu-wb-session-title{color:var(--dcu-sidebar-primary)}.dcu-wb-empty{color:var(--dcu-sidebar-tertiary);font-size:13px;line-height:18px}.dcu-wb-nochat{padding:0 8px 4px 28px;color:var(--dcu-sidebar-tertiary);font-size:14px;line-height:20px}`;
1343
1398
  const WORKSPACE_TREE_STYLE = stylesheet$4 + runningStyles + typographyStyles;
1344
1399
  function storage$1() {
1345
- if (typeof window === "undefined") return void 0;
1346
- try {
1347
- return window.localStorage;
1348
- } catch {
1349
- return;
1350
- }
1400
+ return browserStorage();
1351
1401
  }
1352
1402
  function sameIds(left, right) {
1353
1403
  return left.length === right.length && left.every((id, index) => id === right[index]);
@@ -1367,7 +1417,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1367
1417
  function CodexWorkspaceTree({ wide, useSessions, useWorkspaces, t, archiveSession, deleteSession, deleteWorkspace, forkSession, insertSessionBefore, insertWorkspaceBefore, openPath, openSession, renameSession, renameWorkspace, startSession }) {
1368
1418
  const sessions = useSessions((state) => state);
1369
1419
  const workspaces = useWorkspaces((state) => state);
1370
- const [expanded, setExpanded] = (0, react.useState)({});
1420
+ const [expanded, setExpanded] = (0, react.useState)(() => readTreeExpansionState(storage$1(), WORKSPACE_EXPANSION_STORAGE_KEY));
1371
1421
  const [pinnedWorkspaceIds, setPinnedWorkspaceIdsState] = (0, react.useState)(() => readPinnedWorkspaceIds(storage$1()));
1372
1422
  const pinnedWorkspaceIdsRef = (0, react.useRef)(pinnedWorkspaceIds);
1373
1423
  pinnedWorkspaceIdsRef.current = pinnedWorkspaceIds;
@@ -1422,6 +1472,9 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1422
1472
  const headerMenuPointerAt = (0, react.useRef)(0);
1423
1473
  const [sessionDrag, setSessionDrag] = (0, react.useState)();
1424
1474
  const [sessionDropTarget, setSessionDropTarget] = (0, react.useState)();
1475
+ (0, react.useEffect)(() => {
1476
+ writeTreeExpansionState(storage$1(), WORKSPACE_EXPANSION_STORAGE_KEY, expanded);
1477
+ }, [expanded]);
1425
1478
  (0, react.useEffect)(() => {
1426
1479
  savePinnedWorkspaceIds(storage$1(), pinnedWorkspaceIds);
1427
1480
  if (!pinnedHostHydratedRef.current) return;
@@ -2545,7 +2598,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2545
2598
  //#endregion
2546
2599
  //#region src/client/session-row-actions.tsx
2547
2600
  function storage() {
2548
- return typeof window === "undefined" ? void 0 : window.localStorage;
2601
+ return browserStorage();
2549
2602
  }
2550
2603
  /** 频道/定时共用的本地置顶与未读标记。 */
2551
2604
  function useSessionFlags(current) {
@@ -2796,7 +2849,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2796
2849
  const sessions = useSessions((state) => state);
2797
2850
  const [groups, setGroups] = (0, react.useState)([]);
2798
2851
  const [pollError, setPollError] = (0, react.useState)();
2799
- const [expanded, setExpanded] = (0, react.useState)({});
2852
+ const [expanded, setExpanded] = (0, react.useState)(() => readTreeExpansionState(browserStorage(), CHANNEL_EXPANSION_STORAGE_KEY));
2800
2853
  const flags = useSessionFlags(sessions.current);
2801
2854
  const { showTip, hideTip, dismissTip } = useHoverDispatch();
2802
2855
  const { busy, error, setError, run } = useBusyAction(() => {
@@ -2811,6 +2864,9 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2811
2864
  setMenu(void 0);
2812
2865
  setError(void 0);
2813
2866
  });
2867
+ (0, react.useEffect)(() => {
2868
+ writeTreeExpansionState(browserStorage(), CHANNEL_EXPANSION_STORAGE_KEY, expanded);
2869
+ }, [expanded]);
2814
2870
  (0, react.useEffect)(() => {
2815
2871
  let disposed = false;
2816
2872
  let active;
@@ -2960,6 +3016,23 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2960
3016
  });
2961
3017
  }
2962
3018
  //#endregion
3019
+ //#region src/client/schedule-group-actions.ts
3020
+ /** 尝试归档整组并保留逐项结果,避免首个失败让后续会话永远不执行。 */
3021
+ async function archiveScheduleGroup(sessionIds, archiveSession) {
3022
+ const archivedIds = [];
3023
+ const failedIds = [];
3024
+ for (const sessionId of sessionIds) try {
3025
+ await archiveSession(sessionId);
3026
+ archivedIds.push(sessionId);
3027
+ } catch {
3028
+ failedIds.push(sessionId);
3029
+ }
3030
+ return {
3031
+ archivedIds,
3032
+ failedIds
3033
+ };
3034
+ }
3035
+ //#endregion
2963
3036
  //#region src/client/ScheduleBrowser.tsx
2964
3037
  function ScheduleClock() {
2965
3038
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
@@ -2979,7 +3052,8 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2979
3052
  /** 定时树:数据来自会话快照,行/菜单/悬停与任务树共用。 */
2980
3053
  function ScheduleBrowser(props) {
2981
3054
  const [menu, setMenu] = (0, react.useState)();
2982
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HoverShell, {
3055
+ const [view, setView] = (0, react.useState)("runs");
3056
+ if (props.overviewContent === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HoverShell, {
2983
3057
  blocked: menu !== void 0,
2984
3058
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScheduleBrowserTree, {
2985
3059
  ...props,
@@ -2987,11 +3061,50 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2987
3061
  setMenu
2988
3062
  })
2989
3063
  });
3064
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3065
+ className: "dcu-schedule-browser",
3066
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3067
+ className: "dcu-schedule-views",
3068
+ role: "tablist",
3069
+ "aria-label": props.t("sidebar.scheduleTab"),
3070
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3071
+ type: "button",
3072
+ role: "tab",
3073
+ "aria-selected": view === "runs",
3074
+ onClick: () => {
3075
+ setMenu(void 0);
3076
+ setView("runs");
3077
+ },
3078
+ children: props.t("sidebar.runsTab")
3079
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3080
+ type: "button",
3081
+ role: "tab",
3082
+ "aria-selected": view === "overview",
3083
+ onClick: () => {
3084
+ setMenu(void 0);
3085
+ setView("overview");
3086
+ },
3087
+ children: props.t("sidebar.overviewTab")
3088
+ })]
3089
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3090
+ className: "dcu-schedule-pane",
3091
+ children: view === "runs" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HoverShell, {
3092
+ blocked: menu !== void 0,
3093
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScheduleBrowserTree, {
3094
+ ...props,
3095
+ menu,
3096
+ setMenu
3097
+ })
3098
+ }) : props.overviewContent
3099
+ })]
3100
+ });
2990
3101
  }
2991
- function ScheduleBrowserTree({ openSession, archiveSession, deleteSession, forkSession, renameSession, useSessions, useWorkspaces, t, menu, setMenu }) {
3102
+ function ScheduleBrowserTree({ openSession, archiveSession, deleteSession, forkSession, renameSession, useSessions, useWorkspaces, t, openTaskSettings, menu, setMenu }) {
2992
3103
  const sessions = useSessions((state) => state);
2993
3104
  const workspaces = useWorkspaces((state) => state);
2994
- const [expanded, setExpanded] = (0, react.useState)({});
3105
+ const [expanded, setExpanded] = (0, react.useState)(() => readTreeExpansionState(browserStorage(), SCHEDULE_EXPANSION_STORAGE_KEY));
3106
+ const [groupMenu, setGroupMenu] = (0, react.useState)();
3107
+ const [archiveGroupTarget, setArchiveGroupTarget] = (0, react.useState)();
2995
3108
  const flags = useSessionFlags(sessions.current);
2996
3109
  const { showTip, hideTip, dismissTip } = useHoverDispatch();
2997
3110
  const { busy, error, setError, run } = useBusyAction(() => {
@@ -3006,6 +3119,26 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3006
3119
  setMenu(void 0);
3007
3120
  setError(void 0);
3008
3121
  });
3122
+ const groupMenuItems = [
3123
+ {
3124
+ id: "task-settings",
3125
+ label: t("schedule.taskSettings"),
3126
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSettingsOutline16, { size: 16 })
3127
+ },
3128
+ {
3129
+ type: "separator",
3130
+ id: "group-separator"
3131
+ },
3132
+ {
3133
+ id: "archive-group",
3134
+ label: t("schedule.archiveGroup"),
3135
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOutline20, { size: 16 }),
3136
+ danger: true
3137
+ }
3138
+ ];
3139
+ (0, react.useEffect)(() => {
3140
+ writeTreeExpansionState(browserStorage(), SCHEDULE_EXPANSION_STORAGE_KEY, expanded);
3141
+ }, [expanded]);
3009
3142
  const groups = (0, react.useMemo)(() => {
3010
3143
  const archived = new Set(workspaces.archivedSessionIds ?? []);
3011
3144
  return groupScheduleSessions((sessions.ids ?? Object.keys(sessions.byId)).flatMap((id) => {
@@ -3049,12 +3182,52 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3049
3182
  expanded: isExpanded,
3050
3183
  title: group.label,
3051
3184
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScheduleClock, {}),
3185
+ menuOpen: groupMenu === group.id,
3052
3186
  onToggle: () => {
3053
3187
  setExpanded((current) => ({
3054
3188
  ...current,
3055
3189
  [group.id]: !isExpanded
3056
3190
  }));
3057
- }
3191
+ },
3192
+ actions: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
3193
+ open: groupMenu === group.id,
3194
+ onClose: () => {
3195
+ setGroupMenu(void 0);
3196
+ },
3197
+ items: groupMenuItems,
3198
+ onSelect: (action) => {
3199
+ setGroupMenu(void 0);
3200
+ if (action === "task-settings") {
3201
+ setMenu(void 0);
3202
+ openTaskSettings?.({
3203
+ name: group.label,
3204
+ sessionIds: group.sessions.map((session) => session.id)
3205
+ });
3206
+ }
3207
+ if (action === "archive-group") {
3208
+ setError(void 0);
3209
+ setArchiveGroupTarget({
3210
+ id: group.id,
3211
+ label: group.label,
3212
+ sessionIds: group.sessions.map((session) => session.id)
3213
+ });
3214
+ }
3215
+ },
3216
+ portal: true,
3217
+ dense: true,
3218
+ compact: true,
3219
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3220
+ type: "button",
3221
+ className: "dcu-wb-more",
3222
+ "aria-label": t("schedule.groupActions", { name: group.label }),
3223
+ onClick: (event) => {
3224
+ event.stopPropagation();
3225
+ setMenu(void 0);
3226
+ setGroupMenu((current) => current === group.id ? void 0 : group.id);
3227
+ },
3228
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEllipsisOutline16, { size: 16 })
3229
+ })
3230
+ })
3058
3231
  }), isExpanded && group.sessions.map((session) => {
3059
3232
  const id = session.id;
3060
3233
  const title = session.title;
@@ -3127,19 +3300,77 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3127
3300
  error,
3128
3301
  ...dialogs,
3129
3302
  setError
3303
+ }),
3304
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
3305
+ open: archiveGroupTarget !== void 0,
3306
+ onClose: () => {
3307
+ if (busy !== "archive-group") {
3308
+ setArchiveGroupTarget(void 0);
3309
+ setError(void 0);
3310
+ }
3311
+ },
3312
+ closeLabel: t("sessions.close"),
3313
+ title: t("schedule.archiveGroup"),
3314
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3315
+ className: "dcu-wb-rename-actions",
3316
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3317
+ variant: "outline",
3318
+ disabled: busy === "archive-group",
3319
+ onClick: () => {
3320
+ setArchiveGroupTarget(void 0);
3321
+ setError(void 0);
3322
+ },
3323
+ children: t("sessions.cancel")
3324
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3325
+ variant: "outline",
3326
+ className: "dcu-wb-delete-button",
3327
+ disabled: busy === "archive-group",
3328
+ onClick: () => {
3329
+ if (archiveGroupTarget === void 0) return;
3330
+ const target = archiveGroupTarget;
3331
+ run("archive-group", async () => {
3332
+ const result = await archiveScheduleGroup(target.sessionIds, (id) => archiveSession(id));
3333
+ flags.setPinnedSessionIds((ids) => ids.filter((id) => !result.archivedIds.includes(id)));
3334
+ flags.setUnreadSessionIds((ids) => ids.filter((id) => !result.archivedIds.includes(id)));
3335
+ if (result.failedIds.length > 0) {
3336
+ setArchiveGroupTarget({
3337
+ ...target,
3338
+ sessionIds: result.failedIds
3339
+ });
3340
+ throw new Error(t("schedule.archiveGroupPartial", {
3341
+ archived: result.archivedIds.length,
3342
+ failed: result.failedIds.length
3343
+ }));
3344
+ }
3345
+ setArchiveGroupTarget(void 0);
3346
+ });
3347
+ },
3348
+ children: t("schedule.archiveGroupConfirm")
3349
+ })]
3350
+ }),
3351
+ children: [
3352
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3353
+ className: "dcu-wb-delete-copy",
3354
+ children: archiveGroupTarget === void 0 ? "" : t("schedule.archiveGroupDescription", {
3355
+ name: archiveGroupTarget.label,
3356
+ count: archiveGroupTarget.sessionIds.length
3357
+ })
3358
+ }),
3359
+ busy === "archive-group" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3360
+ className: "dcu-wb-error",
3361
+ role: "status",
3362
+ children: t("schedule.archiveGroupPending")
3363
+ }),
3364
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3365
+ className: "dcu-wb-error",
3366
+ role: "alert",
3367
+ children: t("sessions.failed", { message: error })
3368
+ })
3369
+ ]
3130
3370
  })
3131
3371
  ]
3132
3372
  });
3133
3373
  }
3134
- /** 侧栏拖动手柄持续向左拉过阈值后,松手才收缩。 */
3135
- function shouldCollapseOnSidebarDrag(startX, endX) {
3136
- return startX - endX >= 100 && endX <= 180;
3137
- }
3138
- function isSidebarDragHandle(target) {
3139
- if (target === null || typeof target !== "object" || !("closest" in target)) return false;
3140
- const handle = target.closest("[data-side=\"sidebar\"]");
3141
- return handle !== null && String(handle.className).includes("handle");
3142
- }
3143
3374
  /** 解析宿主 AppFrame 的 grid-template-columns。 */
3144
3375
  function parseSidebarGrid(value) {
3145
3376
  const match = /^(\d+(?:\.\d+)?)px\s+(minmax\(0,\s*1fr\))\s+(\d+(?:\.\d+)?)px$/.exec(value.trim());
@@ -3150,14 +3381,19 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3150
3381
  details: Number(match[3])
3151
3382
  };
3152
3383
  }
3153
- /** 折叠列保持原值;默认展开列收到 Codex 宽度,用户再拉宽时只减去默认多出来的部分。 */
3154
- function slimedSidebarWidth(hostWidth, collapsed) {
3384
+ /**
3385
+ * Translate the DSH host's 264–420px drag range to Codex's measured
3386
+ * 240–520px range, preserving the 280px-host / 275px-Codex default anchor.
3387
+ */
3388
+ function slimedSidebarWidth(hostWidth, collapsed, viewportWidth = Number.POSITIVE_INFINITY) {
3155
3389
  if (collapsed || hostWidth <= 80) return hostWidth;
3156
- if (hostWidth <= 280) return 240;
3157
- return hostWidth - 40;
3390
+ const maximum = Math.max(240, Math.min(520, viewportWidth - 240));
3391
+ const source = Math.max(264, Math.min(420, hostWidth));
3392
+ if (source <= 280) return 240 + (source - 264) * 35 / 16;
3393
+ return 275 + (source - 280) * (maximum - 275) / 140;
3158
3394
  }
3159
- function slimedGridTemplate(tracks, collapsed) {
3160
- return `${slimedSidebarWidth(tracks.sidebar, collapsed)}px ${tracks.middle} ${tracks.details}px`;
3395
+ function slimedGridTemplate(tracks, collapsed, viewportWidth = Number.POSITIVE_INFINITY) {
3396
+ return `${slimedSidebarWidth(tracks.sidebar, collapsed, viewportWidth)}px ${tracks.middle} ${tracks.details}px`;
3161
3397
  }
3162
3398
  function findSidebarFrame(root) {
3163
3399
  const marked = root.querySelector("[data-sidebar-collapsed]");
@@ -3165,15 +3401,20 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3165
3401
  for (const node of root.querySelectorAll("div")) if (parseSidebarGrid(node.style.gridTemplateColumns) !== void 0) return node;
3166
3402
  }
3167
3403
  function applySlimSidebar(frame) {
3168
- if (frame.hasAttribute("data-dragging")) return false;
3404
+ if (frame.hasAttribute("data-dragging")) {
3405
+ frame.removeAttribute("data-dcu-codex-sidebar-grid");
3406
+ return false;
3407
+ }
3169
3408
  const tracks = parseSidebarGrid(frame.style.gridTemplateColumns);
3170
3409
  if (tracks === void 0) return false;
3171
3410
  const collapsed = frame.hasAttribute("data-sidebar-collapsed");
3172
- const next = slimedGridTemplate(tracks, collapsed);
3411
+ const next = slimedGridTemplate(tracks, collapsed, frame.getBoundingClientRect().width);
3412
+ if (frame.getAttribute("data-dcu-codex-sidebar-grid") === frame.style.gridTemplateColumns) return false;
3173
3413
  if (frame.style.gridTemplateColumns === next) return false;
3174
3414
  frame.style.gridTemplateColumns = next;
3415
+ frame.setAttribute("data-dcu-codex-sidebar-grid", next);
3175
3416
  const handle = frame.querySelector("[data-side=\"sidebar\"]");
3176
- if (handle !== null && !collapsed) handle.style.left = `${slimedSidebarWidth(tracks.sidebar, collapsed)}px`;
3417
+ if (handle !== null && !collapsed) handle.style.left = `${slimedSidebarWidth(tracks.sidebar, collapsed, frame.getBoundingClientRect().width)}px`;
3177
3418
  return true;
3178
3419
  }
3179
3420
  function observeSlimSidebar() {
@@ -3229,20 +3470,62 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3229
3470
  };
3230
3471
  }
3231
3472
  //#endregion
3473
+ //#region src/client/sidebar-drag.ts
3474
+ /** Codex collapses the sidebar when a resize drag would make it narrower than 240px. */
3475
+ function shouldCollapseOnSidebarDrag(startWidth, startX, endX) {
3476
+ return startWidth + endX - startX < 240;
3477
+ }
3478
+ function isSidebarDragHandle(target) {
3479
+ if (target === null || typeof target !== "object" || !("closest" in target)) return false;
3480
+ const handle = target.closest("[data-side=\"sidebar\"]");
3481
+ return handle !== null && String(handle.className).includes("handle");
3482
+ }
3483
+ //#endregion
3484
+ //#region src/client/automation-task-settings.ts
3485
+ const AUTOMATION_TASK_SETTINGS_EVENT = "dsh-automation:open-task-settings";
3486
+ const AUTOMATION_TASK_SETTINGS_STORAGE_KEY = "dsh-automation:pending-task-settings";
3487
+ function parseAutomationTaskSettingsRequest(value) {
3488
+ if (value === null || typeof value !== "object") return void 0;
3489
+ const record = value;
3490
+ if (typeof record.name !== "string" || record.name.trim() === "" || !Array.isArray(record.sessionIds) || record.sessionIds.some((id) => typeof id !== "string" || id === "")) return void 0;
3491
+ if (record.automationId !== void 0 && (typeof record.automationId !== "string" || record.automationId.trim() === "")) return void 0;
3492
+ return {
3493
+ ...typeof record.automationId === "string" ? { automationId: record.automationId } : {},
3494
+ name: record.name,
3495
+ sessionIds: [...record.sessionIds]
3496
+ };
3497
+ }
3498
+ /** 定时设置页可能切换分区后才挂载:先保存一次性请求,再通知已挂载实例。 */
3499
+ function requestAutomationTaskSettings(request) {
3500
+ if (typeof window === "undefined") return;
3501
+ const validated = parseAutomationTaskSettingsRequest(request);
3502
+ if (validated === void 0) return;
3503
+ try {
3504
+ window.sessionStorage.setItem(AUTOMATION_TASK_SETTINGS_STORAGE_KEY, JSON.stringify(validated));
3505
+ } catch {}
3506
+ window.dispatchEvent(new CustomEvent(AUTOMATION_TASK_SETTINGS_EVENT, { detail: validated }));
3507
+ }
3508
+ function clearAutomationTaskSettingsRequest() {
3509
+ if (typeof window === "undefined") return;
3510
+ try {
3511
+ window.sessionStorage.removeItem(AUTOMATION_TASK_SETTINGS_STORAGE_KEY);
3512
+ } catch {}
3513
+ }
3514
+ //#endregion
3232
3515
  //#region src/client/CodexSidebar.tsx
3233
3516
  const subscribeEmptyCompanionTabs = () => () => {};
3234
3517
  const getEmptyCompanionTabs = () => EMPTY_COMPANION_TABS;
3235
3518
  const SIDEBAR_COLLAPSE_SETTLE_MS = 140;
3236
3519
  const stylesheet$3 = `
3237
3520
  .dcu-root{--dcu-font:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei UI",sans-serif;--dcu-sidebar-wide-width:240px;--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;overflow:hidden;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)}
3238
- .dcu-expanded-shell{display:flex;width:var(--dcu-sidebar-wide-width);min-height:0;flex:1 0 auto;flex-direction:column;transform-origin:left center;transition:opacity 140ms ease-out,transform 180ms cubic-bezier(.16,1,.3,1);animation:dcu-sidebar-expanded-in 180ms cubic-bezier(.16,1,.3,1)}.dcu-compact-shell{display:none;width:56px}.dcu-root.dcu-collapsing .dcu-expanded-shell{opacity:0;transform:translateX(-6px);pointer-events:none}.dcu-root.dcu-compact .dcu-expanded-shell{display:none}.dcu-root.dcu-compact .dcu-compact-shell{display:flex;min-height:0;flex:1;flex-direction:column;align-items:center;animation:dcu-sidebar-compact-in 140ms ease-out}@keyframes dcu-sidebar-expanded-in{from{opacity:0;transform:translateX(-6px)}to{opacity:1;transform:none}}@keyframes dcu-sidebar-compact-in{from{opacity:0;transform:translateX(-4px)}to{opacity:1;transform:none}}
3521
+ .dcu-expanded-shell{display:flex;width:var(--dcu-sidebar-wide-width);min-height:0;flex:1 1 0;overflow:hidden;flex-direction:column;transform-origin:left center;transition:opacity 140ms ease-out,transform 180ms cubic-bezier(.16,1,.3,1);animation:dcu-sidebar-expanded-in 180ms cubic-bezier(.16,1,.3,1)}.dcu-compact-shell{display:none;width:56px}.dcu-root.dcu-collapsing .dcu-expanded-shell{opacity:0;transform:translateX(-6px);pointer-events:none}.dcu-root.dcu-compact .dcu-expanded-shell{display:none}.dcu-root.dcu-compact .dcu-compact-shell{display:flex;min-height:0;flex:1;flex-direction:column;align-items:center;animation:dcu-sidebar-compact-in 140ms ease-out}@keyframes dcu-sidebar-expanded-in{from{opacity:0;transform:translateX(-6px)}to{opacity:1;transform:none}}@keyframes dcu-sidebar-compact-in{from{opacity:0;transform:translateX(-4px)}to{opacity:1;transform:none}}
3239
3522
  .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}
3240
3523
  .dcu-icon,.dcu-menu button,.dcu-footer-link{appearance:none;border:0;background:transparent;color:inherit;font:inherit;cursor:pointer}.dcu-icon{display:grid;place-items:center;width:36px;height:36px;border-radius:8px;color:var(--dcu-sidebar-icon)}.dcu-head .dcu-icon{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin:0;padding:0;border-radius:50%;line-height:0}.dcu-head .dcu-icon svg{display:block;width:16px;height:16px}
3241
3524
  .dcu-icon:hover,.dcu-menu button:hover:not(:disabled),.dcu-footer-link:hover{background:var(--dcu-sidebar-hover);color:var(--dcu-sidebar-primary)}
3242
3525
  .dcu-menu{padding:0 6px 8px;display:grid;gap:2px}.dcu-menu button,.dcu-footer-link{display:grid;grid-template-columns:20px minmax(0,1fr);column-gap:8px;align-items:center;width:100%;min-height:36px;padding:0 4px;border-radius:8px;color:var(--dcu-sidebar-navigation);font-size:14px;line-height:20px;text-align:left;font-weight:400}
3243
3526
  .dcu-menu-icon{display:grid;place-items:center start;width:20px;height:20px}.dcu-menu-icon svg,.dcu-footer-link svg{display:block;width:16px;height:16px;color:var(--dcu-sidebar-icon)}.dcu-menu button:disabled{color:var(--dcu-sidebar-secondary);cursor:default;opacity:1}.dcu-menu button:disabled svg{color:var(--dcu-sidebar-secondary)}
3244
3527
  .dcu-extension-items{display:grid;gap:1px;margin:1px 0 4px 28px}.dcu-extension-items button{min-height:32px;color:var(--dcu-sidebar-secondary);font-size:13px;font-weight:400}
3245
- .dcu-workspaces{display:flex;min-height:0;flex:1;flex-direction:column;margin-top:2px;padding-top:8px;border-top:1px solid var(--dcu-sidebar-border)}.dcu-workspaces.dcu-workspaces-tabs{padding-top:0;border-top:0}.dcu-im-tabs{display:flex;gap:16px;margin:0 8px 12px;padding:0;border-bottom:1px solid var(--dcu-sidebar-border)}.dcu-im-tab{appearance:none;border:0;background:transparent;color:var(--dcu-sidebar-secondary);padding:8px 0 7px;font:14px/22px var(--dcu-font);font-weight:500;cursor:pointer}.dcu-im-tab[data-on=true]{color:var(--dcu-sidebar-primary);font-weight:600;box-shadow:inset 0 -2px 0 currentColor}.dcu-native-workspaces{display:flex;min-height:0;flex:1}.dcu-native-workspaces>*{min-width:0;flex:1}.dcu-native-workspaces .ima-tabs,.dcu-native-workspaces [role=tablist]{display:none!important}.dcu-foot{display:grid;width:var(--dcu-sidebar-wide-width);gap:4px;padding:8px 6px 12px;border-top:1px solid var(--dcu-sidebar-border);transition:opacity 120ms ease-out,transform 160ms cubic-bezier(.16,1,.3,1)}.dcu-root.dcu-collapsing>.dcu-foot{opacity:0;transform:translateX(-4px);pointer-events:none}.dcu-footer-actions:empty,.dcu-settings-seat:empty{display:none}.dcu-settings-seat>button{width:100%;min-height:36px;padding-left:4px!important;color:var(--dcu-sidebar-navigation);font:14px/20px var(--dcu-font);font-weight:400}.dcu-compact{width:100%;align-items:flex-start;overflow:hidden;padding:10px 0 8px}.dcu-compact-nav{display:flex;flex:1;min-height:0;flex-direction:column;align-items:center;gap:2px;overflow:auto;padding:6px 0}.dcu-compact .dcu-icon{width:36px;height:36px;flex:none}.dcu-compact .dcu-foot{width:36px;margin-top:auto;margin-left:10px;padding:8px 0;border-top:0}.dcu-compact .dcu-settings-seat{width:36px;overflow:hidden}.dcu-compact .dcu-settings-seat>button{display:grid;place-items:center;width:36px;min-height:36px;padding:0!important;font-size:0!important;line-height:0}.dcu-compact .dcu-settings-seat>button svg{width:16px;height:16px}.dcu-compact .dcu-footer-link{display:flex;justify-content:center;width:36px;padding:0;font-size:0}.dcu-compact .dcu-footer-link svg{width:16px;height:16px}
3528
+ .dcu-workspaces{display:flex;min-height:0;flex:1;flex-direction:column;margin-top:2px;padding-top:8px;border-top:1px solid var(--dcu-sidebar-border)}.dcu-workspaces.dcu-workspaces-tabs{padding-top:0;border-top:0}.dcu-im-tabs{display:flex;gap:16px;margin:0 8px 12px;padding:0;border-bottom:1px solid var(--dcu-sidebar-border)}.dcu-im-tab{appearance:none;border:0;background:transparent;color:var(--dcu-sidebar-secondary);padding:8px 0 7px;font:14px/22px var(--dcu-font);font-weight:500;cursor:pointer}.dcu-im-tab[data-on=true]{color:var(--dcu-sidebar-primary);font-weight:600;box-shadow:inset 0 -2px 0 currentColor}.dcu-native-workspaces{display:flex;min-height:0;flex:1}.dcu-native-workspaces>*{min-width:0;flex:1}.dcu-native-workspaces .ima-tabs,.dcu-native-workspaces [role=tablist]{display:none!important}.dcu-schedule-browser{display:flex;min-height:0;flex:1;flex-direction:column}.dcu-native-workspaces .dcu-schedule-views{display:flex!important;flex:none;min-height:30px;margin:0 8px 8px;padding:2px;border:1px solid var(--dcu-sidebar-border);border-radius:8px;background:rgba(255,255,255,.025)}.dcu-schedule-views button{appearance:none;flex:1;min-width:0;height:24px;border:0;border-radius:6px;background:transparent;color:var(--dcu-sidebar-secondary);font:600 12px/18px var(--dcu-font);cursor:pointer}.dcu-schedule-views button[aria-selected=true]{background:var(--dcu-sidebar-hover);color:var(--dcu-sidebar-primary);box-shadow:inset 0 0 0 1px var(--dcu-sidebar-border)}.dcu-schedule-pane{display:flex;min-height:0;flex:1}.dcu-schedule-pane>*{min-width:0;flex:1}.dcu-schedule-pane>[data-slot="sidebar.schedule"]{display:flex!important;width:100%;min-width:0;flex:1}.dcu-schedule-pane>[data-slot="sidebar.schedule"]>.dsh-st-rail{width:100%;min-width:0;padding-right:8px;scrollbar-gutter:auto}.dcu-schedule-pane .dsh-st-overview{padding-right:8px}.dcu-foot{display:grid;width:var(--dcu-sidebar-wide-width);gap:4px;padding:8px 6px 12px;border-top:1px solid var(--dcu-sidebar-border);transition:opacity 120ms ease-out,transform 160ms cubic-bezier(.16,1,.3,1)}.dcu-root.dcu-collapsing>.dcu-foot{opacity:0;transform:translateX(-4px);pointer-events:none}.dcu-footer-actions:empty,.dcu-settings-seat:empty{display:none}.dcu-settings-seat>button{width:100%;min-height:36px;padding-left:4px!important;color:var(--dcu-sidebar-navigation);font:14px/20px var(--dcu-font);font-weight:400}.dcu-compact{width:100%;align-items:flex-start;overflow:hidden;padding:10px 0 8px}.dcu-compact-nav{display:flex;flex:1;min-height:0;flex-direction:column;align-items:center;gap:2px;overflow:auto;padding:6px 0}.dcu-compact .dcu-icon{width:36px;height:36px;flex:none}.dcu-compact .dcu-foot{width:36px;margin-top:auto;margin-left:10px;padding:8px 0;border-top:0}.dcu-compact .dcu-settings-seat{width:36px;overflow:hidden}.dcu-compact .dcu-settings-seat>button{display:grid;place-items:center;width:36px;min-height:36px;padding:0!important;font-size:0!important;line-height:0}.dcu-compact .dcu-settings-seat>button svg{width:16px;height:16px}.dcu-compact .dcu-footer-link{display:flex;justify-content:center;width:36px;padding:0;font-size:0}.dcu-compact .dcu-footer-link svg{width:16px;height:16px}
3246
3529
  .dcu-native-workspaces{flex-direction:column}.dcu-native-workspaces>[data-mcp-connector-top-mount=true]{flex:none}.dcu-root .mcpConnectorLauncher,.dcu-root [data-mcp-connector-top-mount=true]{display:none!important}
3247
3530
  .dcu-dependency-notice{margin:0 10px 8px;border:1px solid var(--dcu-sidebar-border);border-radius:8px;padding:8px;color:var(--dcu-sidebar-secondary);font-size:12px;line-height:18px}
3248
3531
  [role="dialog"][aria-labelledby]{width:min(1000px,calc(100vw - 48px));max-width:calc(100vw - 48px);height:min(800px,calc(100vh - 48px))}
@@ -3548,7 +3831,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3548
3831
  const compact = collapsed || width < 80;
3549
3832
  const [visualCompact, setVisualCompact] = (0, react.useState)(compact);
3550
3833
  const [collapsing, setCollapsing] = (0, react.useState)(false);
3551
- const lastWideWidth = (0, react.useRef)(width >= 80 ? width : 240);
3834
+ const lastWideWidth = (0, react.useRef)(width >= 80 ? width : 275);
3552
3835
  if (!compact) lastWideWidth.current = width;
3553
3836
  const settingsSeat = (0, react.useRef)(null);
3554
3837
  const search = (0, react.useRef)(null);
@@ -3584,6 +3867,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3584
3867
  ]);
3585
3868
  (0, react.useEffect)(() => {
3586
3869
  let startX = 0;
3870
+ let startWidth = 275;
3587
3871
  let dragging = false;
3588
3872
  let pointerId;
3589
3873
  const onDown = (event) => {
@@ -3591,12 +3875,14 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3591
3875
  dragging = true;
3592
3876
  pointerId = event.pointerId;
3593
3877
  startX = event.clientX;
3878
+ const match = (document.querySelector("[data-sidebar-collapsed]") ?? [...document.querySelectorAll("div")].find((node) => node.style.gridTemplateColumns.includes("minmax(0, 1fr)")))?.style.gridTemplateColumns.match(/^(\d+(?:\.\d+)?)px\s/);
3879
+ startWidth = match === null || match === void 0 ? 275 : Number(match[1]);
3594
3880
  };
3595
3881
  const onUp = (event) => {
3596
3882
  if (!dragging || event.pointerId !== pointerId) return;
3597
3883
  dragging = false;
3598
3884
  pointerId = void 0;
3599
- if (!shouldCollapseOnSidebarDrag(startX, event.clientX)) return;
3885
+ if (!shouldCollapseOnSidebarDrag(startWidth, startX, event.clientX)) return;
3600
3886
  window.requestAnimationFrame(() => {
3601
3887
  window.requestAnimationFrame(() => {
3602
3888
  toggleSidebar();
@@ -3645,7 +3931,33 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3645
3931
  wide: true,
3646
3932
  expandSidebar
3647
3933
  }), [expandSidebar, renderSlot]);
3648
- const sidebarStyle = { "--dcu-sidebar-wide-width": `${lastWideWidth.current}px` };
3934
+ const scheduleOverviewSlot = (0, react.useMemo)(() => renderSlot("sidebar.schedule", {
3935
+ wide: true,
3936
+ expandSidebar,
3937
+ openSession,
3938
+ archiveSession,
3939
+ deleteSession,
3940
+ forkSession,
3941
+ renameSession,
3942
+ openPath,
3943
+ useSessions,
3944
+ useWorkspaces,
3945
+ view: "overview",
3946
+ showViewSwitch: false
3947
+ }), [
3948
+ archiveSession,
3949
+ companionTabs.schedule,
3950
+ deleteSession,
3951
+ expandSidebar,
3952
+ forkSession,
3953
+ openPath,
3954
+ openSession,
3955
+ renameSession,
3956
+ renderSlot,
3957
+ useSessions,
3958
+ useWorkspaces
3959
+ ]);
3960
+ const sidebarStyle = { "--dcu-sidebar-wide-width": `${slimedSidebarWidth(lastWideWidth.current, false, window.innerWidth)}px` };
3649
3961
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
3650
3962
  className: `dcu-root${visualCompact ? " dcu-compact" : ""}${collapsing ? " dcu-collapsing" : ""}`,
3651
3963
  style: sidebarStyle,
@@ -3806,7 +4118,16 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3806
4118
  renameSession,
3807
4119
  useSessions,
3808
4120
  useWorkspaces,
3809
- t
4121
+ t,
4122
+ overviewContent: scheduleOverviewSlot,
4123
+ openTaskSettings: (request) => {
4124
+ openSettingsSection(settingsSeat.current, t("sidebar.schedule"), () => {
4125
+ clearAutomationTaskSettingsRequest();
4126
+ selectSection(t("about.nav"));
4127
+ }, () => {
4128
+ requestAutomationTaskSettings(request);
4129
+ });
4130
+ }
3810
4131
  })
3811
4132
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3812
4133
  className: "dcu-native-workspaces",
@@ -4385,6 +4706,8 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4385
4706
  "sidebar.tasksTab": "任务",
4386
4707
  "sidebar.channelsTab": "频道",
4387
4708
  "sidebar.scheduleTab": "定时",
4709
+ "sidebar.runsTab": "执行记录",
4710
+ "sidebar.overviewTab": "任务总览",
4388
4711
  "channels.empty": "还没有频道会话。先在设置 → IM助理 里连接渠道。",
4389
4712
  "channels.loadError": "无法读取频道会话。",
4390
4713
  "channel.dingtalk": "钉钉",
@@ -4395,6 +4718,13 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4395
4718
  "channel.qq": "QQ",
4396
4719
  "channel.telegram": "Telegram",
4397
4720
  "schedule.empty": "还没有定时任务运行记录。",
4721
+ "schedule.groupActions": "{name} 的任务操作",
4722
+ "schedule.taskSettings": "任务设置",
4723
+ "schedule.archiveGroup": "归档整组会话",
4724
+ "schedule.archiveGroupConfirm": "确认归档",
4725
+ "schedule.archiveGroupDescription": "将归档“{name}”下的 {count} 个会话。归档后可以在“设置 → 已归档”中恢复。",
4726
+ "schedule.archiveGroupPending": "正在归档整组会话…",
4727
+ "schedule.archiveGroupPartial": "已归档 {archived} 个会话,另有 {failed} 个失败。可以重试剩余会话。",
4398
4728
  "search.placeholder": "搜索会话、设置和操作",
4399
4729
  "search.empty": "没有匹配的结果。",
4400
4730
  "search.sessions": "会话",
@@ -4508,6 +4838,8 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4508
4838
  "sidebar.tasksTab": "Tasks",
4509
4839
  "sidebar.channelsTab": "Channels",
4510
4840
  "sidebar.scheduleTab": "Schedule",
4841
+ "sidebar.runsTab": "Run history",
4842
+ "sidebar.overviewTab": "Task overview",
4511
4843
  "channels.empty": "No channel conversations yet. Connect a channel in Settings → IM Assistant.",
4512
4844
  "channels.loadError": "Could not load channel conversations.",
4513
4845
  "channel.dingtalk": "DingTalk",
@@ -4518,6 +4850,13 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4518
4850
  "channel.qq": "QQ",
4519
4851
  "channel.telegram": "Telegram",
4520
4852
  "schedule.empty": "No scheduled-task runs yet.",
4853
+ "schedule.groupActions": "Task actions for {name}",
4854
+ "schedule.taskSettings": "Task settings",
4855
+ "schedule.archiveGroup": "Archive all conversations",
4856
+ "schedule.archiveGroupConfirm": "Archive all",
4857
+ "schedule.archiveGroupDescription": "Archive all {count} conversations for “{name}”. You can restore them later in Settings → Archived.",
4858
+ "schedule.archiveGroupPending": "Archiving conversations…",
4859
+ "schedule.archiveGroupPartial": "Archived {archived} conversations; {failed} failed. You can retry the remaining conversations.",
4521
4860
  "search.placeholder": "Search conversations, settings, and actions",
4522
4861
  "search.empty": "No matching results.",
4523
4862
  "search.sessions": "Conversations",
@@ -4695,11 +5034,14 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4695
5034
  if (typeof document === "undefined" || document.body === null) return () => {};
4696
5035
  let applying = false;
4697
5036
  let frame;
5037
+ const pendingRoots = /* @__PURE__ */ new Set([document]);
4698
5038
  const apply = () => {
4699
5039
  if (applying) return;
4700
5040
  applying = true;
5041
+ const roots = [...pendingRoots];
5042
+ pendingRoots.clear();
4701
5043
  try {
4702
- applySettingsNavIcons(document);
5044
+ for (const root of roots) applySettingsNavIcons(root);
4703
5045
  } finally {
4704
5046
  applying = false;
4705
5047
  }
@@ -4712,7 +5054,18 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4712
5054
  });
4713
5055
  };
4714
5056
  apply();
4715
- const observer = new MutationObserver(schedule);
5057
+ const observer = new MutationObserver((records) => {
5058
+ for (const record of records) {
5059
+ const button = (record.target.nodeType === 1 ? record.target : record.target.parentElement)?.closest("[role=\"dialog\"] nav button");
5060
+ if (button !== null && button !== void 0) pendingRoots.add(button.closest("[role=\"dialog\"]") ?? document);
5061
+ for (const node of record.addedNodes) {
5062
+ if (!(node instanceof Element)) continue;
5063
+ if (node.matches("[role=\"dialog\"] nav button")) pendingRoots.add(node.closest("[role=\"dialog\"]") ?? document);
5064
+ else if (node.querySelector("[role=\"dialog\"] nav button") !== null) pendingRoots.add(node);
5065
+ }
5066
+ }
5067
+ if (pendingRoots.size > 0) schedule();
5068
+ });
4716
5069
  observer.observe(document.body, {
4717
5070
  childList: true,
4718
5071
  subtree: true
@@ -4790,12 +5143,15 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4790
5143
  if (typeof document === "undefined" || document.body === null) return () => {};
4791
5144
  let applying = false;
4792
5145
  let frame;
5146
+ const pendingRoots = /* @__PURE__ */ new Set([document]);
4793
5147
  const apply = () => {
4794
5148
  if (applying) return;
4795
5149
  applying = true;
4796
5150
  const active = locale.getSnapshot?.().active ?? document.documentElement.lang ?? window.navigator.language ?? "zh";
5151
+ const roots = [...pendingRoots];
5152
+ pendingRoots.clear();
4797
5153
  try {
4798
- localizePermissionLabels(document, active);
5154
+ for (const root of roots) localizePermissionLabels(root, active);
4799
5155
  } finally {
4800
5156
  applying = false;
4801
5157
  }
@@ -4808,13 +5164,26 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
4808
5164
  });
4809
5165
  };
4810
5166
  apply();
4811
- const observer = new MutationObserver(schedule);
5167
+ const observer = new MutationObserver((records) => {
5168
+ for (const record of records) {
5169
+ const control = (record.target.nodeType === 1 ? record.target : record.target.parentElement)?.closest(INTERACTIVE_SELECTOR);
5170
+ if (control !== null && control !== void 0) pendingRoots.add(control);
5171
+ for (const node of record.addedNodes) {
5172
+ if (!(node instanceof Element)) continue;
5173
+ if (node.matches(INTERACTIVE_SELECTOR) || node.querySelector(INTERACTIVE_SELECTOR) !== null) pendingRoots.add(node);
5174
+ }
5175
+ }
5176
+ if (pendingRoots.size > 0) schedule();
5177
+ });
4812
5178
  observer.observe(document.body, {
4813
5179
  childList: true,
4814
5180
  characterData: true,
4815
5181
  subtree: true
4816
5182
  });
4817
- const unsubscribe = locale.subscribe?.(schedule) ?? (() => {});
5183
+ const unsubscribe = locale.subscribe?.(() => {
5184
+ pendingRoots.add(document);
5185
+ schedule();
5186
+ }) ?? (() => {});
4818
5187
  return () => {
4819
5188
  observer.disconnect();
4820
5189
  unsubscribe();
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { mkdir, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
- import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
4
+ import { basename, dirname, isAbsolute, join, resolve, sep, win32 } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { randomUUID } from "node:crypto";
7
7
  //#region src/dependencies.ts
@@ -514,6 +514,28 @@ async function installDependenciesLocked(ids, requestHotUpdate) {
514
514
  return dependencyStatuses();
515
515
  }
516
516
  //#endregion
517
+ //#region src/explorer-path-policy.ts
518
+ function normalizedWindowsPath(path) {
519
+ if (!win32.isAbsolute(path)) return void 0;
520
+ const normalized = win32.normalize(path.trim()).replace(/[\\/]+$/, "");
521
+ const root = win32.parse(normalized).root;
522
+ if (root.startsWith("\\\\") || normalized.toLocaleLowerCase("en-US") === root.replace(/[\\/]+$/, "").toLocaleLowerCase("en-US")) return;
523
+ return normalized;
524
+ }
525
+ /**
526
+ * 仅允许打开注册表中已有的工作区根目录。
527
+ * 不接受子目录、盘符根或 UNC,避免同源页面把 Host 端点当成任意路径启动器。
528
+ */
529
+ function authorizedExplorerWorkspacePath(path, workspaceRoots) {
530
+ const target = normalizedWindowsPath(path);
531
+ if (target === void 0) return void 0;
532
+ const targetKey = target.toLocaleLowerCase("en-US");
533
+ for (const root of workspaceRoots) {
534
+ const normalizedRoot = normalizedWindowsPath(root);
535
+ if (normalizedRoot !== void 0 && normalizedRoot.toLocaleLowerCase("en-US") === targetKey) return normalizedRoot;
536
+ }
537
+ }
538
+ //#endregion
517
539
  //#region src/host-services.ts
518
540
  function requireService(ctx, key, method) {
519
541
  const service = ctx.get(key);
@@ -525,7 +547,8 @@ function hostServices(ctx) {
525
547
  return {
526
548
  webServer: requireService(ctx, "webServer", "register"),
527
549
  agents: requireService(ctx, "agents", "get"),
528
- tools: requireService(ctx, "tools", "schemas")
550
+ tools: requireService(ctx, "tools", "schemas"),
551
+ workspaceRegistry: requireService(ctx, "workspaceRegistry", "list")
529
552
  };
530
553
  }
531
554
  //#endregion
@@ -869,7 +892,8 @@ async function readRequestBody(request, maxBytes = maxPreferencesBodyBytes) {
869
892
  const inject = [
870
893
  "webServer",
871
894
  "agents",
872
- "tools"
895
+ "tools",
896
+ "workspaceRegistry"
873
897
  ];
874
898
  /** 提供不泄露地址、命令和凭证的连接器目录。 */
875
899
  function apply(ctx) {
@@ -1100,7 +1124,16 @@ function apply(ctx) {
1100
1124
  response.end(JSON.stringify({ error: "目录路径无效。" }));
1101
1125
  return;
1102
1126
  }
1103
- await foregroundExplorer.open(body.path);
1127
+ const authorizedPath = authorizedExplorerWorkspacePath(body.path, host.workspaceRegistry.list().map((workspace) => workspace.path));
1128
+ if (authorizedPath === void 0) {
1129
+ response.writeHead(403, {
1130
+ "content-type": "application/json; charset=utf-8",
1131
+ "cache-control": "no-store"
1132
+ });
1133
+ response.end(JSON.stringify({ error: "仅允许打开已注册的工作区目录。" }));
1134
+ return;
1135
+ }
1136
+ await foregroundExplorer.open(authorizedPath);
1104
1137
  response.writeHead(200, {
1105
1138
  "content-type": "application/json; charset=utf-8",
1106
1139
  "cache-control": "no-store"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-codex-ui",
3
- "version": "0.2.88",
3
+ "version": "0.2.90",
4
4
  "description": "以 Codex 风格重构 DSH Web 侧栏的独立客户端插件",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "homepage": "https://github.com/MichengAI/dsh-codex-ui#readme",
18
18
  "type": "module",
19
+ "packageManager": "pnpm@11.22.0",
19
20
  "main": "dist/index.mjs",
20
21
  "types": "dist/index.d.mts",
21
22
  "exports": {
@@ -56,7 +57,7 @@
56
57
  "scripts": {
57
58
  "build": "tsc --noEmit && tsdown",
58
59
  "typecheck": "tsc --noEmit",
59
- "test": "pnpm run typecheck && vitest run tests/client-runtime.integration.spec.ts && tsdown && node tests/run-assertions.mjs"
60
+ "test": "pnpm run typecheck && vitest run && tsdown && node tests/run-assertions.mjs"
60
61
  },
61
62
  "peerDependencies": {
62
63
  "@deepseek-ai/cordis": ">=4.0.1 <5.0.0",