@michengai/dsh-codex-ui 0.2.107 → 0.2.108

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,21 @@
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
+ ## suite-installer-v0.1.27 - 2026-09-07
8
+
9
+ - Updated the bundled Codex UI to `0.2.108`, including group rename/delete menus and aligned group counts; refreshed exact member versions from the official npm registry.
10
+
11
+ ## 0.2.108 - 2026-09-07
12
+
13
+ - Added rename and delete actions to each custom workspace group; the Projects header now keeps only the create-group button. Renaming preserves group membership, ordering, and expansion.
14
+ - Aligned custom-group and Ungrouped counts, kept long names from crowding menu buttons, and improved keyboard focus recovery after renaming.
15
+ - Prevented IME confirmation from submitting new groups and kept rejected names from interfering with preference synchronization.
16
+ - Made case-insensitive name checks independent of system locale while preserving legacy stored group names and memberships.
17
+
18
+ ## suite-installer-v0.1.26 - 2026-09-07
19
+
20
+ - Refreshed direct member pins from the official npm registry, including IM Connect `0.1.37`, which withdraws its unfinished proactive delivery feature.
21
+
7
22
  ## suite-installer-v0.1.25 - 2026-09-07
8
23
 
9
24
  - Updated the bundled Codex UI to `0.2.107` and refreshed direct member versions from the official npm registry for reproducible installation.
@@ -4,6 +4,21 @@
4
4
 
5
5
  本日志记录 DSH Codex UI 及其一键安装器的最近发布;更早的变更可查看 [Git 提交历史](https://github.com/MichengAI/dsh-codex-ui/commits/main)。
6
6
 
7
+ ## suite-installer-v0.1.27 - 2026-09-07
8
+
9
+ - 将套件中的 Codex UI 更新至 `0.2.108`,包含分组重命名、删除菜单和数量对齐优化;从官方 npm 刷新并锁定成员版本。
10
+
11
+ ## 0.2.108 - 2026-09-07
12
+
13
+ - 为每个自定义工作区分组新增重命名和删除菜单;项目标题栏仅保留新增分组按钮。重命名保留项目归属、排序和展开状态。
14
+ - 对齐自定义分组与“未分组”的数量,避免长名称挤出菜单按钮,并完善重命名后的键盘焦点恢复。
15
+ - 防止新建分组时输入法选词误触发提交,修复名称校验失败影响偏好同步的问题。
16
+ - 统一不依赖系统语言环境的大小写判重规则,同时兼容保留旧分组名称和成员数据。
17
+
18
+ ## suite-installer-v0.1.26 - 2026-09-07
19
+
20
+ - 从官方 npm 刷新直装成员版本,包含 IM Connect `0.1.37`,该版本已撤回未完成的主动投递功能。
21
+
7
22
  ## suite-installer-v0.1.25 - 2026-09-07
8
23
 
9
24
  - 将套件中的 Codex UI 更新至 `0.2.107`,并从官方 npm 刷新直装成员版本,保证安装可复现。
package/lib/client.js CHANGED
@@ -863,8 +863,8 @@ window.__ModuleLoader__.load({
863
863
  this.code = code;
864
864
  }
865
865
  };
866
- /** 对持久化分组做严格校验,避免一个项目同时出现在多个分组。 */
867
- function parseWorkspaceGroups(value) {
866
+ /** 校验结构和归属;存储兼容模式保留旧 locale 下合法的大小写变体名称。 */
867
+ function parseGroups(value, preserveCaseVariants) {
868
868
  if (!Array.isArray(value) || value.length > 100) return void 0;
869
869
  const groupIds = /* @__PURE__ */ new Set();
870
870
  const groupTitles = /* @__PURE__ */ new Set();
@@ -876,13 +876,14 @@ window.__ModuleLoader__.load({
876
876
  const id = typeof group.id === "string" ? group.id.trim() : "";
877
877
  const title = typeof group.title === "string" ? group.title.trim() : "";
878
878
  if (id === "" || id.length > 128 || title === "" || title.length > 80) return void 0;
879
- if (groupIds.has(id) || groupTitles.has(title.toLocaleLowerCase())) return void 0;
879
+ const titleKey = preserveCaseVariants ? title : title.toLowerCase();
880
+ if (groupIds.has(id) || groupTitles.has(titleKey)) return void 0;
880
881
  if (!Array.isArray(group.workspaceIds) || group.workspaceIds.length > 1e3) return void 0;
881
882
  const normalizedIds = [...new Set(group.workspaceIds)];
882
883
  if (!normalizedIds.every((workspaceId) => typeof workspaceId === "string" && workspaceId.trim() !== "" && workspaceId.length <= 256)) return void 0;
883
884
  if (normalizedIds.some((workspaceId) => workspaceIds.has(workspaceId))) return void 0;
884
885
  groupIds.add(id);
885
- groupTitles.add(title.toLocaleLowerCase());
886
+ groupTitles.add(titleKey);
886
887
  normalizedIds.forEach((workspaceId) => workspaceIds.add(workspaceId));
887
888
  groups.push({
888
889
  id,
@@ -892,15 +893,30 @@ window.__ModuleLoader__.load({
892
893
  }
893
894
  return groups;
894
895
  }
896
+ /** 旧数据可往返保存且不改名;新名称的大小写判重由创建、重命名操作执行。 */
897
+ function parseStoredWorkspaceGroups(value) {
898
+ return parseGroups(value, true);
899
+ }
895
900
  /** 新建空分组;项目只有被显式移动后才会进入其中。 */
896
901
  function createWorkspaceGroup(groups, group) {
897
- const next = parseWorkspaceGroups([...groups, {
902
+ if (groups.some((existing) => existing.title.toLowerCase() === group.title.trim().toLowerCase())) throw new WorkspaceGroupError("group-invalid");
903
+ const next = parseStoredWorkspaceGroups([...groups, {
898
904
  ...group,
899
905
  workspaceIds: []
900
906
  }]);
901
907
  if (next === void 0) throw new WorkspaceGroupError("group-invalid");
902
908
  return next;
903
909
  }
910
+ /** 仅更改名称,保留分组标识、成员及其顺序。 */
911
+ function renameWorkspaceGroup(groups, groupId, title) {
912
+ if (!groups.some((group) => group.id === groupId)) throw new WorkspaceGroupError("group-missing");
913
+ const normalizedTitle = title.trim();
914
+ if (normalizedTitle === "" || normalizedTitle.length > 80 || groups.some((group) => group.id !== groupId && group.title.toLowerCase() === normalizedTitle.toLowerCase())) throw new WorkspaceGroupError("group-invalid");
915
+ return groups.map((group) => group.id === groupId ? {
916
+ ...group,
917
+ title: normalizedTitle
918
+ } : group);
919
+ }
904
920
  /** 将项目放入指定分组;未传分组时退回未分组区。 */
905
921
  function assignWorkspaceToGroup(groups, workspaceId, groupId) {
906
922
  if (workspaceId.trim() === "" || workspaceId.length > 256) throw new WorkspaceGroupError("workspace-invalid");
@@ -1029,7 +1045,7 @@ window.__ModuleLoader__.load({
1029
1045
  if (payload === null || typeof payload !== "object") throw new Error("置顶偏好响应格式无效。");
1030
1046
  const record = payload;
1031
1047
  const workspaceGroupsSupported = Object.prototype.hasOwnProperty.call(record, "workspaceGroups");
1032
- const workspaceGroups = workspaceGroupsSupported ? parseWorkspaceGroups(record.workspaceGroups) : [];
1048
+ const workspaceGroups = workspaceGroupsSupported ? parseStoredWorkspaceGroups(record.workspaceGroups) : [];
1033
1049
  if (typeof record.exists !== "boolean" || !Array.isArray(record.pinnedWorkspaceIds) || !record.pinnedWorkspaceIds.every((id) => typeof id === "string") || workspaceGroups === void 0) throw new Error("置顶偏好响应格式无效。");
1034
1050
  return {
1035
1051
  exists: record.exists,
@@ -1039,7 +1055,7 @@ window.__ModuleLoader__.load({
1039
1055
  };
1040
1056
  }
1041
1057
  async function writeHostWorkspacePreferences(pinnedWorkspaceIds, workspaceGroups, fetcher = fetch) {
1042
- const groups = parseWorkspaceGroups([...workspaceGroups]);
1058
+ const groups = parseStoredWorkspaceGroups([...workspaceGroups]);
1043
1059
  if (groups === void 0) throw new Error("工作区分组数据无效。");
1044
1060
  const response = await fetcher(WORKSPACE_PREFERENCES_ENDPOINT, {
1045
1061
  method: "PUT",
@@ -1100,7 +1116,7 @@ window.__ModuleLoader__.load({
1100
1116
  pendingHostSync: false
1101
1117
  };
1102
1118
  const record = value;
1103
- const workspaceGroups = record.version === 1 ? parseWorkspaceGroups(record.workspaceGroups) : void 0;
1119
+ const workspaceGroups = record.version === 1 ? parseStoredWorkspaceGroups(record.workspaceGroups) : void 0;
1104
1120
  if (workspaceGroups === void 0 || typeof record.pendingHostSync !== "boolean") return {
1105
1121
  workspaceGroups: [],
1106
1122
  pendingHostSync: false
@@ -1118,7 +1134,7 @@ window.__ModuleLoader__.load({
1118
1134
  }
1119
1135
  function saveWorkspaceGroupsCache(storage, groups, pendingHostSync) {
1120
1136
  if (storage === void 0) return;
1121
- const workspaceGroups = parseWorkspaceGroups([...groups]);
1137
+ const workspaceGroups = parseStoredWorkspaceGroups([...groups]);
1122
1138
  if (workspaceGroups === void 0) return;
1123
1139
  try {
1124
1140
  storage.setItem(WORKSPACE_GROUPS_STORAGE_KEY, JSON.stringify({
@@ -2199,6 +2215,11 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2199
2215
  .dcu-wb-section+.dcu-wb-section{margin-top:12px}
2200
2216
  .dcu-wb-section-head{position:relative;display:flex;align-items:center;min-height:24px;padding:0;border-radius:6px}.dcu-wb-section-head:hover .dcu-wb-actions{display:flex}.dcu-wb-section-label{position:relative;display:flex;align-items:center;gap:4px;flex:1;min-width:0;min-height:24px;border:0;padding:2px 4px 2px 12px;background:transparent;color:var(--dcu-sidebar-tertiary);font:13px/20px var(--dcu-font,inherit);font-weight:400;letter-spacing:0;text-align:left;cursor:pointer}.dcu-wb-section-caret{display:grid;place-items:center;flex:none;width:12px;height:12px;color:currentColor;opacity:0;transform:rotate(0deg);transform-origin:50% 50%;transition:opacity 120ms ease,transform 160ms ease}.dcu-wb-section-head .dcu-wb-section-caret{position:absolute;left:0;top:6px}.dcu-wb-section-caret svg{display:block}.dcu-wb-section-head:hover .dcu-wb-section-caret,.dcu-wb-section-label:focus-visible .dcu-wb-section-caret{opacity:.78}.dcu-wb-section-label[aria-expanded=true] .dcu-wb-section-caret{transform:rotate(90deg)}.dcu-wb-section-body{display:block}.dcu-wb-section-body[data-open=false]{display:none}.dcu-wb-section-body>div{min-height:0}.dcu-wb-section-body[data-open=true]>div{animation:dcu-wb-section-in 140ms cubic-bezier(.16,1,.3,1)}@keyframes dcu-wb-section-in{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}@media (prefers-reduced-motion:reduce){.dcu-wb-section-caret{transition:none}.dcu-wb-section-body[data-open=true]>div{animation:none}}
2201
2217
  .dcu-wb-project{position:relative;min-width:0}
2218
+ .dcu-wb-collection-head .dcu-wb-more{opacity:0;pointer-events:none}
2219
+ .dcu-wb-collection-head:hover .dcu-wb-more,.dcu-wb-collection-head:focus-within .dcu-wb-more,.dcu-wb-collection-head .dcu-wb-more[aria-expanded=true]{opacity:1;pointer-events:auto}
2220
+ @media (pointer:coarse){.dcu-wb-collection-head .dcu-wb-more{opacity:1;pointer-events:auto}}
2221
+ .dcu-wb-collection-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2222
+ .dcu-wb-collection-actions{flex-shrink:0;width:20px}
2202
2223
  .dcu-wb-collections{display:grid;min-width:0;gap:4px}.dcu-wb-collection,.dcu-wb-ungrouped{position:relative;min-width:0;overflow:visible}.dcu-wb-collection-head{display:flex;align-items:center;min-height:28px;padding:0 4px;border-radius:6px;transition:background 120ms ease}.dcu-wb-collection-head:hover,.dcu-wb-collection-head:focus-within,.dcu-wb-collection-head.dcu-wb-group-drop{background:var(--dcu-sidebar-hover)}.dcu-wb-collection-head.dcu-wb-group-drop{outline:1px solid var(--dsw-alias-state-business-primary)}.dcu-wb-collection-head[draggable=true],.dcu-wb-collection-head[draggable=true] .dcu-wb-collection-label{cursor:grab}.dcu-wb-collection-head[draggable=true]:active,.dcu-wb-collection-head[draggable=true]:active .dcu-wb-collection-label{cursor:grabbing}.dcu-wb-collection-label{display:flex;align-items:center;gap:5px;min-width:0;flex:1;border:0;padding:4px;background:transparent;color:var(--dcu-sidebar-primary);font:13px/20px var(--dcu-font,inherit);text-align:left;cursor:pointer}.dcu-wb-collection-label .dcu-wb-section-caret{position:static;order:-1;opacity:.78}.dcu-wb-collection-label[aria-expanded=true] .dcu-wb-section-caret{transform:rotate(90deg)}.dcu-wb-collection-count{margin-left:auto;color:var(--dcu-sidebar-secondary);font-variant-numeric:tabular-nums}.dcu-wb-collection-body{position:relative;min-width:0;padding-left:12px}.dcu-wb-collection-body::before{content:"";position:absolute;left:31px;top:0;bottom:8px;width:1px;background:var(--dcu-sidebar-border)}.dcu-wb-group-member{position:relative}.dcu-wb-group-member.dcu-wb-drop::before,.dcu-wb-collection.dcu-wb-group-order-drop::before,.dcu-wb-ungrouped.dcu-wb-group-order-drop::before{content:"";position:absolute;z-index:2;left:7px;right:8px;top:-8px;height:8px;pointer-events:none;background:radial-gradient(circle at 4px 50%,transparent 1.75px,var(--dsw-alias-state-business-primary) 2px 3.75px,transparent 4px),linear-gradient(var(--dsw-alias-state-business-primary),var(--dsw-alias-state-business-primary)) 10px 50%/calc(100% - 10px) 2px no-repeat}.dcu-wb-group-member.dcu-wb-drop-after::before{top:auto;bottom:-8px}.dcu-wb-collection-body .dcu-wb-project-head{padding-left:12px}
2203
2224
  .dcu-wb-collection.dcu-wb-workspace-move-drop,.dcu-wb-ungrouped.dcu-wb-workspace-move-drop{border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 9%,transparent);outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
2204
2225
  .dcu-wb-project-head,.dcu-wb-session{position:relative;display:flex;align-items:center;gap:6px;width:100%;min-width:0;border-radius:10px;padding:0 8px;color:var(--dcu-sidebar-primary);cursor:pointer}
@@ -2305,10 +2326,10 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2305
2326
  setPinnedWorkspaceIdsState(next);
2306
2327
  };
2307
2328
  const setWorkspaceGroups = (update) => {
2308
- pinnedHostDirtyRef.current = true;
2309
- workspaceGroupsPendingHostSyncRef.current = true;
2310
2329
  const current = workspaceGroupsRef.current;
2311
2330
  const next = typeof update === "function" ? update(current) : update;
2331
+ pinnedHostDirtyRef.current = true;
2332
+ workspaceGroupsPendingHostSyncRef.current = true;
2312
2333
  workspaceGroupsRef.current = next;
2313
2334
  setWorkspaceGroupsState(next);
2314
2335
  };
@@ -2353,7 +2374,12 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2353
2374
  const [createGroupOpen, setCreateGroupOpen] = (0, react.useState)(false);
2354
2375
  const [groupTitleDraft, setGroupTitleDraft] = (0, react.useState)("");
2355
2376
  const [deleteGroupId, setDeleteGroupId] = (0, react.useState)();
2356
- const [groupMenuOpen, setGroupMenuOpen] = (0, react.useState)(false);
2377
+ const [groupMenuId, setGroupMenuId] = (0, react.useState)();
2378
+ const [renameGroupId, setRenameGroupId] = (0, react.useState)();
2379
+ const [renameGroupDraft, setRenameGroupDraft] = (0, react.useState)("");
2380
+ const [renameGroupError, setRenameGroupError] = (0, react.useState)();
2381
+ const groupActionTriggerRef = (0, react.useRef)();
2382
+ const projectsSectionButtonRef = (0, react.useRef)(null);
2357
2383
  (0, react.useEffect)(() => {
2358
2384
  writeTreeExpansionState(storage$1(), WORKSPACE_EXPANSION_STORAGE_KEY, expanded);
2359
2385
  }, [expanded]);
@@ -2611,6 +2637,28 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
2611
2637
  setError(userErrorText(reason, t));
2612
2638
  }
2613
2639
  };
2640
+ const closeRenameGroup = () => {
2641
+ setRenameGroupId(void 0);
2642
+ setRenameGroupDraft("");
2643
+ setRenameGroupError(void 0);
2644
+ const trigger = groupActionTriggerRef.current;
2645
+ const focusTarget = trigger?.isConnected ? trigger : projectsSectionButtonRef.current;
2646
+ groupActionTriggerRef.current = void 0;
2647
+ focusTarget?.focus();
2648
+ };
2649
+ const submitRenameGroup = () => {
2650
+ if (renameGroupId === void 0 || renameGroupDraft.trim() === "") return;
2651
+ if (workspaceGroupsRef.current.find((group) => group.id === renameGroupId)?.title === renameGroupDraft.trim()) {
2652
+ closeRenameGroup();
2653
+ return;
2654
+ }
2655
+ try {
2656
+ setWorkspaceGroups((current) => renameWorkspaceGroup(current, renameGroupId, renameGroupDraft));
2657
+ closeRenameGroup();
2658
+ } catch (reason) {
2659
+ setRenameGroupError(userErrorText(reason, t));
2660
+ }
2661
+ };
2614
2662
  const sectionOpen = (id) => expanded[`section:${id}`] ?? true;
2615
2663
  const toggleSection = (id) => {
2616
2664
  setExpanded((current) => ({
@@ -3234,7 +3282,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3234
3282
  setWorkspaceDropTarget(void 0);
3235
3283
  setWorkspaceGroups((current) => current.find((candidate) => candidate.workspaceIds.includes(dragged))?.id === groupId ? moveWorkspaceGroupMember(current, dragged, groupId) : assignWorkspaceToGroup(current, dragged, groupId));
3236
3284
  },
3237
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3285
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3238
3286
  className: `dcu-wb-collection-head${workspaceGroupDragId === groupId ? " dcu-wb-dragging" : ""}`,
3239
3287
  draggable: true,
3240
3288
  onDragStart: (event) => {
@@ -3252,7 +3300,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3252
3300
  setWorkspaceGroupDragId(void 0);
3253
3301
  setWorkspaceGroupDropTarget(void 0);
3254
3302
  },
3255
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3303
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3256
3304
  type: "button",
3257
3305
  className: "dcu-wb-collection-label",
3258
3306
  "aria-expanded": isExpanded,
@@ -3277,13 +3325,74 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3277
3325
  })
3278
3326
  })
3279
3327
  }),
3280
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: group.title }),
3328
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3329
+ className: "dcu-wb-collection-title",
3330
+ children: group.title
3331
+ }),
3281
3332
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3282
3333
  className: "dcu-wb-collection-count",
3283
3334
  children: members.length
3284
3335
  })
3285
3336
  ]
3286
- })
3337
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3338
+ className: "dcu-wb-collection-actions",
3339
+ onClick: (event) => {
3340
+ event.stopPropagation();
3341
+ },
3342
+ onDragStart: (event) => {
3343
+ event.preventDefault();
3344
+ event.stopPropagation();
3345
+ },
3346
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
3347
+ open: groupMenuId === groupId,
3348
+ onClose: () => {
3349
+ setGroupMenuId(void 0);
3350
+ },
3351
+ items: [
3352
+ {
3353
+ id: "rename",
3354
+ label: t("workspace.renameGroup"),
3355
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, { size: 16 })
3356
+ },
3357
+ {
3358
+ type: "separator",
3359
+ id: "group-delete-separator"
3360
+ },
3361
+ {
3362
+ id: "delete",
3363
+ label: t("workspace.deleteGroupAction"),
3364
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 16 }),
3365
+ danger: true
3366
+ }
3367
+ ],
3368
+ onSelect: (action) => {
3369
+ setGroupMenuId(void 0);
3370
+ if (action === "rename") {
3371
+ setRenameGroupId(groupId);
3372
+ setRenameGroupDraft(group.title);
3373
+ setRenameGroupError(void 0);
3374
+ }
3375
+ if (action === "delete") setDeleteGroupId(groupId);
3376
+ },
3377
+ portal: true,
3378
+ dense: true,
3379
+ compact: true,
3380
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3381
+ type: "button",
3382
+ className: "dcu-wb-more",
3383
+ draggable: false,
3384
+ "aria-haspopup": "menu",
3385
+ "aria-expanded": groupMenuId === groupId,
3386
+ "aria-label": t("workspace.groupActions", { name: group.title }),
3387
+ title: t("workspace.groupActions", { name: group.title }),
3388
+ onClick: (event) => {
3389
+ groupActionTriggerRef.current = event.currentTarget;
3390
+ setGroupMenuId((current) => current === groupId ? void 0 : groupId);
3391
+ },
3392
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEllipsisOutline16, { size: 16 })
3393
+ })
3394
+ })
3395
+ })]
3287
3396
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DisclosureBody, {
3288
3397
  className: "dcu-wb-collection-body",
3289
3398
  open: isExpanded,
@@ -3397,9 +3506,9 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3397
3506
  setWorkspaceGroups((current) => assignWorkspaceToGroup(current, dragged));
3398
3507
  run("workspace-order", () => insertWorkspaceBefore(dragged));
3399
3508
  },
3400
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3509
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3401
3510
  className: "dcu-wb-collection-head",
3402
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3511
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3403
3512
  type: "button",
3404
3513
  className: "dcu-wb-collection-label",
3405
3514
  "aria-expanded": ungroupedExpanded,
@@ -3430,7 +3539,10 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3430
3539
  children: ungroupedGroups.length
3431
3540
  })
3432
3541
  ]
3433
- })
3542
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3543
+ className: "dcu-wb-collection-actions",
3544
+ "aria-hidden": "true"
3545
+ })]
3434
3546
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DisclosureBody, {
3435
3547
  className: "dcu-wb-collection-body",
3436
3548
  open: ungroupedExpanded,
@@ -3611,6 +3723,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3611
3723
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3612
3724
  type: "button",
3613
3725
  className: "dcu-wb-section-label",
3726
+ ref: projectsSectionButtonRef,
3614
3727
  "aria-expanded": sectionOpen("projects"),
3615
3728
  onClick: () => {
3616
3729
  toggleSection("projects");
@@ -3632,9 +3745,9 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3632
3745
  })
3633
3746
  })
3634
3747
  })]
3635
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3748
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3636
3749
  className: "dcu-wb-actions",
3637
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3750
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3638
3751
  type: "button",
3639
3752
  className: "dcu-wb-more",
3640
3753
  "aria-label": t("workspace.createGroup"),
@@ -3646,36 +3759,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3646
3759
  setCreateGroupOpen(true);
3647
3760
  },
3648
3761
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 16 })
3649
- }), workspaceGroups.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
3650
- open: groupMenuOpen,
3651
- onClose: () => {
3652
- setGroupMenuOpen(false);
3653
- },
3654
- items: workspaceGroups.map((group) => ({
3655
- id: group.id,
3656
- label: t("workspace.deleteGroup", { name: group.title }),
3657
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 16 }),
3658
- danger: true
3659
- })),
3660
- onSelect: (id) => {
3661
- setDeleteGroupId(id);
3662
- setGroupMenuOpen(false);
3663
- },
3664
- portal: true,
3665
- dense: true,
3666
- compact: true,
3667
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3668
- type: "button",
3669
- className: "dcu-wb-more",
3670
- "aria-label": t("workspace.manageGroups"),
3671
- title: t("workspace.manageGroups"),
3672
- onClick: (event) => {
3673
- event.stopPropagation();
3674
- setGroupMenuOpen((current) => !current);
3675
- },
3676
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEllipsisOutline16, { size: 16 })
3677
- })
3678
- })]
3762
+ })
3679
3763
  })]
3680
3764
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3681
3765
  className: "dcu-wb-section-body",
@@ -3881,6 +3965,57 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3881
3965
  setPinnedWorkspaceIds((ids) => togglePinnedWorkspace(ids, id));
3882
3966
  }
3883
3967
  }),
3968
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
3969
+ open: renameGroupId !== void 0,
3970
+ onClose: closeRenameGroup,
3971
+ closeLabel: t("sessions.close"),
3972
+ title: t("workspace.renameGroup"),
3973
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3974
+ className: "dcu-wb-rename-actions",
3975
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3976
+ variant: "outline",
3977
+ onClick: closeRenameGroup,
3978
+ children: t("sessions.cancel")
3979
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3980
+ variant: "primary",
3981
+ disabled: renameGroupDraft.trim() === "",
3982
+ onClick: submitRenameGroup,
3983
+ children: t("sessions.save")
3984
+ })]
3985
+ }),
3986
+ children: [
3987
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3988
+ htmlFor: "dcu-workspace-group-name",
3989
+ children: t("workspace.groupName")
3990
+ }),
3991
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3992
+ id: "dcu-workspace-group-name",
3993
+ className: "dcu-wb-rename-input",
3994
+ "aria-label": t("workspace.groupName"),
3995
+ maxLength: 80,
3996
+ value: renameGroupDraft,
3997
+ autoFocus: true,
3998
+ onFocus: (event) => {
3999
+ event.target.select();
4000
+ },
4001
+ onChange: (event) => {
4002
+ setRenameGroupDraft(event.target.value);
4003
+ setRenameGroupError(void 0);
4004
+ },
4005
+ onKeyDown: (event) => {
4006
+ if (event.key === "Enter" && !event.nativeEvent.isComposing && event.keyCode !== 229) {
4007
+ event.preventDefault();
4008
+ submitRenameGroup();
4009
+ }
4010
+ }
4011
+ }),
4012
+ renameGroupError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4013
+ className: "dcu-wb-error",
4014
+ role: "alert",
4015
+ children: t("sessions.failed", { message: renameGroupError })
4016
+ })
4017
+ ]
4018
+ }),
3884
4019
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
3885
4020
  open: createGroupOpen,
3886
4021
  onClose: () => {
@@ -3918,7 +4053,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3918
4053
  setError(void 0);
3919
4054
  },
3920
4055
  onKeyDown: (event) => {
3921
- if (event.key === "Enter") {
4056
+ if (event.key === "Enter" && !event.nativeEvent.isComposing && event.keyCode !== 229) {
3922
4057
  event.preventDefault();
3923
4058
  submitCreateGroup();
3924
4059
  }
@@ -6580,7 +6715,8 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
6580
6715
  "workspace.deleteGroup": "删除分组“{name}”",
6581
6716
  "workspace.deleteGroupAction": "删除分组",
6582
6717
  "workspace.deleteGroupDescription": "删除分组只会将其中项目移至未分组,不会删除项目或会话。",
6583
- "workspace.manageGroups": "管理分组",
6718
+ "workspace.groupActions": "分组“{name}”的操作",
6719
+ "workspace.renameGroup": "重命名分组",
6584
6720
  "workspace.recent": "最近",
6585
6721
  "workspace.recentEmpty": "无聊天",
6586
6722
  "workspace.noChat": "没有聊天",
@@ -6780,7 +6916,8 @@ html[data-dcu-official-turn-navigator-supported=true] .dcu-turn-navigator,html:h
6780
6916
  "workspace.deleteGroup": "Delete group {name}",
6781
6917
  "workspace.deleteGroupAction": "Delete group",
6782
6918
  "workspace.deleteGroupDescription": "Deleting this group moves its projects to Ungrouped. It does not delete projects or conversations.",
6783
- "workspace.manageGroups": "Manage groups",
6919
+ "workspace.groupActions": "Actions for group {name}",
6920
+ "workspace.renameGroup": "Rename group",
6784
6921
  "workspace.recent": "Recents",
6785
6922
  "workspace.recentEmpty": "No chats",
6786
6923
  "workspace.noChat": "No chats",
package/lib/index.mjs CHANGED
@@ -1446,8 +1446,8 @@ async function moveSessionToWorkspace(services, sessionId, targetWorkspaceId, op
1446
1446
  movingSessions.delete(sessionId);
1447
1447
  }
1448
1448
  }
1449
- /** 对持久化分组做严格校验,避免一个项目同时出现在多个分组。 */
1450
- function parseWorkspaceGroups(value) {
1449
+ /** 校验结构和归属;存储兼容模式保留旧 locale 下合法的大小写变体名称。 */
1450
+ function parseGroups(value, preserveCaseVariants) {
1451
1451
  if (!Array.isArray(value) || value.length > 100) return void 0;
1452
1452
  const groupIds = /* @__PURE__ */ new Set();
1453
1453
  const groupTitles = /* @__PURE__ */ new Set();
@@ -1459,13 +1459,14 @@ function parseWorkspaceGroups(value) {
1459
1459
  const id = typeof group.id === "string" ? group.id.trim() : "";
1460
1460
  const title = typeof group.title === "string" ? group.title.trim() : "";
1461
1461
  if (id === "" || id.length > 128 || title === "" || title.length > 80) return void 0;
1462
- if (groupIds.has(id) || groupTitles.has(title.toLocaleLowerCase())) return void 0;
1462
+ const titleKey = preserveCaseVariants ? title : title.toLowerCase();
1463
+ if (groupIds.has(id) || groupTitles.has(titleKey)) return void 0;
1463
1464
  if (!Array.isArray(group.workspaceIds) || group.workspaceIds.length > 1e3) return void 0;
1464
1465
  const normalizedIds = [...new Set(group.workspaceIds)];
1465
1466
  if (!normalizedIds.every((workspaceId) => typeof workspaceId === "string" && workspaceId.trim() !== "" && workspaceId.length <= 256)) return void 0;
1466
1467
  if (normalizedIds.some((workspaceId) => workspaceIds.has(workspaceId))) return void 0;
1467
1468
  groupIds.add(id);
1468
- groupTitles.add(title.toLocaleLowerCase());
1469
+ groupTitles.add(titleKey);
1469
1470
  normalizedIds.forEach((workspaceId) => workspaceIds.add(workspaceId));
1470
1471
  groups.push({
1471
1472
  id,
@@ -1475,6 +1476,10 @@ function parseWorkspaceGroups(value) {
1475
1476
  }
1476
1477
  return groups;
1477
1478
  }
1479
+ /** 旧数据可往返保存且不改名;新名称的大小写判重由创建、重命名操作执行。 */
1480
+ function parseStoredWorkspaceGroups(value) {
1481
+ return parseGroups(value, true);
1482
+ }
1478
1483
  //#endregion
1479
1484
  //#region src/workspace-preferences.ts
1480
1485
  const WORKSPACE_PREFERENCES_FILE = ".dsh-codex-ui-preferences.json";
@@ -1499,7 +1504,7 @@ function parseWorkspacePreferences(value) {
1499
1504
  workspaceGroups: []
1500
1505
  };
1501
1506
  if (record.version !== 2) return void 0;
1502
- const workspaceGroups = parseWorkspaceGroups(record.workspaceGroups);
1507
+ const workspaceGroups = parseStoredWorkspaceGroups(record.workspaceGroups);
1503
1508
  return workspaceGroups === void 0 ? void 0 : {
1504
1509
  version: 2,
1505
1510
  pinnedWorkspaceIds,
@@ -1531,7 +1536,7 @@ let writeQueue = Promise.resolve();
1531
1536
  /** 串行、原子保存,避免快速拖动排序产生乱序或半截 JSON。 */
1532
1537
  function writeWorkspacePreferences(pinnedWorkspaceIds, workspaceGroups = [], path = workspacePreferencesPath()) {
1533
1538
  const normalized = parsePinnedWorkspaceIds([...pinnedWorkspaceIds]);
1534
- const normalizedGroups = parseWorkspaceGroups([...workspaceGroups]);
1539
+ const normalizedGroups = parseStoredWorkspaceGroups([...workspaceGroups]);
1535
1540
  if (normalized === void 0 || normalizedGroups === void 0) return Promise.reject(/* @__PURE__ */ new Error("工作区偏好数据无效。"));
1536
1541
  const task = writeQueue.catch(() => void 0).then(async () => {
1537
1542
  await mkdir(dirname(path), { recursive: true });
@@ -1817,8 +1822,9 @@ function apply(ctx) {
1817
1822
  const record = body !== null && typeof body === "object" ? body : void 0;
1818
1823
  const pinnedWorkspaceIds = record === void 0 ? void 0 : parsePinnedWorkspaceIds(record.pinnedWorkspaceIds);
1819
1824
  const existing = await readWorkspacePreferences();
1820
- const workspaceGroups = record === void 0 ? void 0 : "workspaceGroups" in record ? parseWorkspaceGroups(record.workspaceGroups) : existing.workspaceGroups;
1821
- if (pinnedWorkspaceIds === void 0 || workspaceGroups === void 0) {
1825
+ const workspaceGroups = record === void 0 ? void 0 : "workspaceGroups" in record ? parseStoredWorkspaceGroups(record.workspaceGroups) : existing.workspaceGroups;
1826
+ const introducesTitleConflict = workspaceGroups?.some((group) => !existing.workspaceGroups.some((previous) => previous.id === group.id && previous.title === group.title) && workspaceGroups.some((other) => other.id !== group.id && other.title.toLowerCase() === group.title.toLowerCase())) === true;
1827
+ if (pinnedWorkspaceIds === void 0 || workspaceGroups === void 0 || introducesTitleConflict) {
1822
1828
  response.writeHead(400, {
1823
1829
  "content-type": "application/json; charset=utf-8",
1824
1830
  "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.107",
3
+ "version": "0.2.108",
4
4
  "description": "以 Codex 风格重构 DSH Web 侧栏的独立客户端插件",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {