@michengai/dsh-codex-ui 0.2.58 → 0.2.60

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.
Binary file
package/dist/client.js CHANGED
@@ -262,6 +262,7 @@ window.__ModuleLoader__.load({
262
262
  //#endregion
263
263
  //#region src/client/session-manager.ts
264
264
  const SESSION_PINS_STORAGE_KEY = "dsh.session-pins.v1";
265
+ const SESSION_SECTION_PINS_STORAGE_KEY = "dsh-codex-ui.pinned-section-sessions.v1";
265
266
  const SESSION_UNREAD_STORAGE_KEY = "dsh.session-unread.v1";
266
267
  function normalizeSessionIds(ids) {
267
268
  return [...new Set(ids.filter((id) => id.trim() !== ""))];
@@ -294,6 +295,15 @@ window.__ModuleLoader__.load({
294
295
  url.searchParams.set("session", sessionId);
295
296
  return url.toString();
296
297
  }
298
+ /** 把会话插入置顶列表的指定位置;省略锚点时追加到末尾。 */
299
+ function insertSessionId(ids, id, beforeId) {
300
+ const next = ids.filter((item) => item !== id);
301
+ if (beforeId === void 0) return [...next, id];
302
+ if (beforeId === id) return ids.includes(id) ? [...ids] : [...next, id];
303
+ const index = next.indexOf(beforeId);
304
+ next.splice(index < 0 ? next.length : index, 0, id);
305
+ return next;
306
+ }
297
307
  //#endregion
298
308
  //#region src/client/session-tree.tsx
299
309
  function PinIcon() {
@@ -430,19 +440,33 @@ window.__ModuleLoader__.load({
430
440
  function pointerMenuRect(x, y) {
431
441
  return new DOMRect(x, y, 0, 0);
432
442
  }
433
- function SessionRow({ id, title, selected, menuOpen, pinned, unread, running, t, menuItems, onOpen, onMenuChange, onSelectAction, onPin, onArchive, onHover, onLeave, onContextMenu, menuPoint }) {
443
+ function SessionRow({ id, title, selected, menuOpen, pinned, unread, running, t, menuItems, onOpen, onMenuChange, onSelectAction, onPin, onArchive, onHover, onLeave, onContextMenu, menuPoint, subtitle, flat, draggable, dropActive, onDragStart, onDragEnd, onDragOver, onDrop }) {
434
444
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
435
- className: `dcu-wb-session${selected ? " dcu-wb-selected" : ""}${menuOpen ? " dcu-wb-menu-open" : ""}`,
445
+ className: `dcu-wb-session${selected ? " dcu-wb-selected" : ""}${menuOpen ? " dcu-wb-menu-open" : ""}${dropActive === true ? " dcu-wb-drop" : ""}${flat === true ? " dcu-wb-session-flat" : ""}`,
436
446
  role: "treeitem",
437
447
  "aria-selected": selected,
448
+ draggable,
449
+ onDragStart,
450
+ onDragEnd,
451
+ onDragOver,
452
+ onDrop,
438
453
  onClick: onOpen,
439
454
  onContextMenu,
440
455
  onMouseEnter: onHover,
441
456
  onMouseLeave: onLeave,
442
457
  children: [
443
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
458
+ subtitle !== void 0 && subtitle !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
459
+ className: "dcu-wb-session-copy",
460
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
461
+ className: "dcu-wb-session-title",
462
+ children: title.split(/\r?\n/)[0] ?? title
463
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
464
+ className: "dcu-wb-session-sub",
465
+ children: subtitle
466
+ })]
467
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
444
468
  className: "dcu-wb-session-title",
445
- children: title
469
+ children: title.split(/\r?\n/)[0] ?? title
446
470
  }),
447
471
  pinned && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
448
472
  className: "dcu-wb-pin",
@@ -1097,6 +1121,65 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1097
1121
  return item === void 0 ? [] : [item];
1098
1122
  });
1099
1123
  }
1124
+ /** 置顶区的会话按拖入顺序展示;不因父项目已置顶而隐藏。 */
1125
+ function standalonePinnedSessionIds(pinnedSessionIds) {
1126
+ return [...pinnedSessionIds];
1127
+ }
1128
+ /** 置顶区已有会话时,不再同时展示任何项目文件夹,避免旧置顶项目叠成两条。 */
1129
+ function workspaceIdsHiddenByPinnedSessions(workspaces, pinnedSessionIds) {
1130
+ if (pinnedSessionIds.length === 0) return [];
1131
+ return workspaces.map((workspace) => String(workspace.workspaceId));
1132
+ }
1133
+ const SESSION_DRAG_TYPE = "application/x-dcu-session";
1134
+ const WORKSPACE_DRAG_TYPE = "application/x-dcu-workspace";
1135
+ const SESSION_DRAG_PREFIX = "dcu-session:";
1136
+ const WORKSPACE_DRAG_PREFIX = "dcu-workspace:";
1137
+ function dragTypes(data) {
1138
+ return data === void 0 ? [] : Array.from(data.types);
1139
+ }
1140
+ function textPayload(data) {
1141
+ try {
1142
+ return data?.getData("text/plain") ?? "";
1143
+ } catch {
1144
+ return "";
1145
+ }
1146
+ }
1147
+ /** 拖过置顶区时用来决定是否 preventDefault;只看 types,不读数据。 */
1148
+ function isSidebarItemDrag(data) {
1149
+ const types = dragTypes(data);
1150
+ return types.includes("application/x-dcu-session") || types.includes("application/x-dcu-workspace") || types.includes("text/plain");
1151
+ }
1152
+ /** 写入会话拖拽载荷;drop 时以这个为准,不依赖尚未刷新的 React 状态。 */
1153
+ function writeSessionDrag(data, sessionId, title) {
1154
+ data.effectAllowed = "move";
1155
+ data.setData("text/plain", `${SESSION_DRAG_PREFIX}${sessionId}`);
1156
+ data.setData(SESSION_DRAG_TYPE, sessionId);
1157
+ }
1158
+ function writeWorkspaceDrag(data, workspaceId, title) {
1159
+ data.effectAllowed = "move";
1160
+ data.setData("text/plain", `${WORKSPACE_DRAG_PREFIX}${workspaceId}`);
1161
+ data.setData(WORKSPACE_DRAG_TYPE, workspaceId);
1162
+ }
1163
+ function readSessionDrag(data, fallback) {
1164
+ try {
1165
+ const typed = data?.getData(SESSION_DRAG_TYPE);
1166
+ if (typed !== void 0 && typed.trim() !== "") return typed;
1167
+ } catch {}
1168
+ const text = textPayload(data);
1169
+ if (text.startsWith(SESSION_DRAG_PREFIX)) return text.slice(12);
1170
+ return fallback !== void 0 && fallback.trim() !== "" ? fallback : void 0;
1171
+ }
1172
+ /** 会话拖拽优先;有会话载荷时不得再把父项目置顶。 */
1173
+ function readWorkspaceDrag(data, fallback) {
1174
+ if (readSessionDrag(data) !== void 0) return void 0;
1175
+ try {
1176
+ const typed = data?.getData(WORKSPACE_DRAG_TYPE);
1177
+ if (typed !== void 0 && typed.trim() !== "") return typed;
1178
+ } catch {}
1179
+ const text = textPayload(data);
1180
+ if (text.startsWith(WORKSPACE_DRAG_PREFIX)) return text.slice(14);
1181
+ return fallback !== void 0 && fallback.trim() !== "" ? fallback : void 0;
1182
+ }
1100
1183
  //#endregion
1101
1184
  //#region src/client/CodexWorkspaceBrowser.tsx
1102
1185
  const stylesheet$4 = `
@@ -1111,13 +1194,13 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1111
1194
  .dcu-wb-project-head:hover,.dcu-wb-project-head.dcu-wb-menu-open,.dcu-wb-session:hover,.dcu-wb-session.dcu-wb-selected,.dcu-wb-session.dcu-wb-menu-open{background:var(--dcu-sidebar-hover)}.dcu-wb-project-head+.dcu-wb-session,.dcu-wb-project-head+.dcu-wb-nochat{margin-top:4px}.dcu-wb-session+.dcu-wb-session{margin-top:2px}
1112
1195
  .dcu-wb-project-head[draggable=true],.dcu-wb-session[draggable=true]{cursor:grab}
1113
1196
  .dcu-wb-project-head[draggable=true]:active,.dcu-wb-session[draggable=true]:active{cursor:grabbing}
1114
- .dcu-wb-section,.dcu-wb-section:focus,.dcu-wb-section:focus-visible,.dcu-wb-project-head:focus,.dcu-wb-session:focus{outline:0}.dcu-wb-pin-end{position:relative;height:8px}.dcu-wb-project-head.dcu-wb-drop::before,.dcu-wb-session.dcu-wb-drop::before,.dcu-wb-pin-end.dcu-wb-drop::before,.dcu-wb-section[data-pin-over=true] .dcu-wb-section-head::after{content:"";position:absolute;left:8px;right:8px;top:-4px;height:8px;pointer-events:none;background:radial-gradient(circle at 4px 50%,var(--dsw-alias-state-business-primary) 3.25px,transparent 3.45px),linear-gradient(var(--dsw-alias-state-business-primary),var(--dsw-alias-state-business-primary)) 10px 50%/calc(100% - 10px) 2px no-repeat}.dcu-wb-drag-ghost{position:fixed;top:-120px;left:-240px;z-index:10040;max-width:220px;height:32px;padding:0 10px;border:1px solid var(--dcu-sidebar-border);border-radius:8px;background:var(--dcu-sidebar-hover);color:var(--dcu-sidebar-primary);font:13px/32px var(--dcu-font,inherit);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:none}
1197
+ .dcu-wb-section,.dcu-wb-section:focus,.dcu-wb-section:focus-visible,.dcu-wb-project-head:focus,.dcu-wb-session:focus{outline:0}.dcu-wb-pin-end{position:relative;height:8px}.dcu-wb-project-head.dcu-wb-drop::before,.dcu-wb-session.dcu-wb-drop::before,.dcu-wb-pin-end.dcu-wb-drop::before,.dcu-wb-section[data-pin-over=true] .dcu-wb-section-head::after{content:"";position:absolute;left:8px;right:8px;top:-4px;height:8px;pointer-events:none;background:radial-gradient(circle at 4px 50%,var(--dsw-alias-state-business-primary) 3.25px,transparent 3.45px),linear-gradient(var(--dsw-alias-state-business-primary),var(--dsw-alias-state-business-primary)) 10px 50%/calc(100% - 10px) 2px no-repeat}.dcu-wb-project-head.dcu-wb-drop-after::before,.dcu-wb-session.dcu-wb-drop-after::before{top:auto;bottom:-4px}.dcu-wb-dragging{opacity:.28}.dcu-wb-drag-ghost{position:fixed;top:-120px;left:-240px;z-index:10040;max-width:220px;height:32px;padding:0 10px;border:1px solid var(--dcu-sidebar-border);border-radius:8px;background:var(--dcu-sidebar-hover);color:var(--dcu-sidebar-primary);font:13px/32px var(--dcu-font,inherit);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:none}
1115
1198
  .dcu-wb-folder{display:grid;place-items:center;flex:none;width:16px;height:20px;color:var(--dcu-sidebar-icon)}.dcu-wb-brand{display:block;width:16px;height:16px}
1116
1199
  .dcu-wb-project-current .dcu-wb-folder{color:var(--dsw-alias-state-business-primary)}
1117
1200
  .dcu-wb-project-title,.dcu-wb-session-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;line-height:20px}
1118
1201
  .dcu-wb-project-title{flex:1;font-weight:550;color:var(--dcu-sidebar-primary)}
1119
1202
  .dcu-wb-session{position:relative;min-height:32px;gap:0;padding-left:28px}
1120
- .dcu-wb-session-title{flex:1;margin-left:0}
1203
+ .dcu-wb-session-title{flex:1;margin-left:0}.dcu-wb-session-copy{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center}.dcu-wb-session-sub{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dcu-sidebar-tertiary);font-size:12px;line-height:16px}.dcu-wb-session-flat,.dcu-wb-session-top{padding-left:8px}
1121
1204
  .dcu-wb-pin{display:grid;place-items:center;flex:none;width:16px;height:20px;margin-left:6px;color:var(--dcu-sidebar-tertiary)}
1122
1205
  .dcu-wb-pin svg,.dcu-wb-quick-pin svg{width:13px;height:13px;fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.4}
1123
1206
  .dcu-wb-actions{display:none;align-items:center;gap:8px;flex:none}
@@ -1162,6 +1245,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1162
1245
  const [expanded, setExpanded] = (0, react.useState)({});
1163
1246
  const [pinnedWorkspaceIds, setPinnedWorkspaceIds] = (0, react.useState)(() => readPinnedWorkspaceIds(storage$1()));
1164
1247
  const [pinnedSessionIds, setPinnedSessionIds] = (0, react.useState)(() => readSessionIds(storage$1(), SESSION_PINS_STORAGE_KEY));
1248
+ const [sectionSessionIds, setSectionSessionIds] = (0, react.useState)(() => readSessionIds(storage$1(), SESSION_SECTION_PINS_STORAGE_KEY));
1165
1249
  const [unreadSessionIds, setUnreadSessionIds] = (0, react.useState)(() => readSessionIds(storage$1(), SESSION_UNREAD_STORAGE_KEY));
1166
1250
  const [menu, setMenu] = (0, react.useState)();
1167
1251
  const [renameTarget, setRenameTarget] = (0, react.useState)();
@@ -1176,6 +1260,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1176
1260
  const [error, setError] = (0, react.useState)();
1177
1261
  const [workspaceDragId, setWorkspaceDragId] = (0, react.useState)();
1178
1262
  const [workspaceDropId, setWorkspaceDropId] = (0, react.useState)();
1263
+ const [workspaceDropAfter, setWorkspaceDropAfter] = (0, react.useState)(false);
1179
1264
  const [headerMenu, setHeaderMenu] = (0, react.useState)();
1180
1265
  const headerMenuRef = (0, react.useRef)();
1181
1266
  headerMenuRef.current = headerMenu;
@@ -1189,6 +1274,13 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1189
1274
  (0, react.useEffect)(() => {
1190
1275
  writeSessionIds(storage$1(), SESSION_PINS_STORAGE_KEY, pinnedSessionIds);
1191
1276
  }, [pinnedSessionIds]);
1277
+ (0, react.useEffect)(() => {
1278
+ writeSessionIds(storage$1(), SESSION_SECTION_PINS_STORAGE_KEY, sectionSessionIds);
1279
+ }, [sectionSessionIds]);
1280
+ (0, react.useEffect)(() => {
1281
+ if (sectionSessionIds.length === 0) return;
1282
+ setPinnedWorkspaceIds((ids) => ids.length === 0 ? ids : []);
1283
+ }, [sectionSessionIds]);
1192
1284
  (0, react.useEffect)(() => {
1193
1285
  writeSessionIds(storage$1(), SESSION_UNREAD_STORAGE_KEY, unreadSessionIds);
1194
1286
  }, [unreadSessionIds]);
@@ -1275,11 +1367,14 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1275
1367
  sessions.current,
1276
1368
  menu
1277
1369
  ]);
1278
- const projectPinned = (id) => pinnedWorkspaceIds.includes(String(id));
1279
- const pinnedGroups = orderByIds(groups.items, pinnedWorkspaceIds, (workspace) => String(workspace.workspaceId));
1370
+ const pinSectionSessions = standalonePinnedSessionIds(sectionSessionIds);
1371
+ const hiddenPinnedWorkspaceIds = new Set(workspaceIdsHiddenByPinnedSessions(groups.items, pinSectionSessions));
1372
+ const projectPinned = (id) => pinnedWorkspaceIds.includes(String(id)) && !hiddenPinnedWorkspaceIds.has(String(id));
1373
+ const pinnedGroups = orderByIds(groups.items, pinnedWorkspaceIds, (workspace) => String(workspace.workspaceId)).filter((workspace) => !hiddenPinnedWorkspaceIds.has(String(workspace.workspaceId)));
1280
1374
  const regularGroups = groups.items.filter((workspace) => !projectPinned(workspace.workspaceId));
1375
+ const pinDragActive = workspaceDragId !== void 0 || sessionDrag !== void 0;
1281
1376
  const assignedIds = workspaces.items.flatMap((workspace) => workspace.sessionIds.map((id) => String(id)));
1282
- 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));
1377
+ const recentIds = ungroupedSessionIds(sessions.ids ?? Object.keys(sessions.byId), sessions.byId, assignedIds, workspaces.archivedSessionIds).filter((id) => !pinSectionSessions.includes(id)).sort((left, right) => (sessions.byId[right]?.updatedAt ?? 0) - (sessions.byId[left]?.updatedAt ?? 0));
1283
1378
  const run = async (key, action) => {
1284
1379
  setBusy(key);
1285
1380
  setError(void 0);
@@ -1317,6 +1412,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1317
1412
  if (target.kind === "session") {
1318
1413
  await deleteSession(target.id);
1319
1414
  setPinnedSessionIds((ids) => ids.filter((id) => id !== target.id));
1415
+ setSectionSessionIds((ids) => ids.filter((id) => id !== target.id));
1320
1416
  setUnreadSessionIds((ids) => ids.filter((id) => id !== target.id));
1321
1417
  } else {
1322
1418
  await deleteWorkspace(target.id);
@@ -1331,10 +1427,10 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1331
1427
  await (0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(value);
1332
1428
  });
1333
1429
  };
1334
- const toggleGroup = (workspaceId) => {
1430
+ const toggleGroup = (key, defaultOpen = true) => {
1335
1431
  setExpanded((current) => ({
1336
1432
  ...current,
1337
- [workspaceId]: !(current[workspaceId] ?? true)
1433
+ [key]: !(current[key] ?? defaultOpen)
1338
1434
  }));
1339
1435
  };
1340
1436
  const sectionOpen = (id) => expanded[`section:${id}`] ?? true;
@@ -1385,8 +1481,14 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1385
1481
  const pinWorkspaceAt = (id, beforeId) => {
1386
1482
  setPinnedWorkspaceIds((ids) => insertPinnedWorkspace(ids, id, beforeId));
1387
1483
  };
1484
+ const pinSessionAt = (id, beforeId) => {
1485
+ setSectionSessionIds((ids) => insertSessionId(ids, id, beforeId));
1486
+ setPinnedWorkspaceIds((ids) => ids.length === 0 ? ids : []);
1487
+ };
1388
1488
  const renderGroup = (workspace, zone) => {
1389
- const isExpanded = expanded[workspace.workspaceId] ?? true;
1489
+ const expandKey = zone === "pinned" ? `pin:${workspace.workspaceId}` : String(workspace.workspaceId);
1490
+ const isExpanded = expanded[expandKey] ?? zone !== "pinned";
1491
+ const shownIds = workspace.visibleIds.filter((id) => !pinSectionSessions.includes(id));
1390
1492
  const menuOpen = menu?.type === "workspace" && menu.id === workspace.workspaceId;
1391
1493
  const menuAt = menuOpen && menu.x !== void 0 && menu.y !== void 0 ? {
1392
1494
  x: menu.x,
@@ -1396,27 +1498,43 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1396
1498
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1397
1499
  className: `dcu-wb-project${isCurrentWorkspace ? " dcu-wb-project-current" : ""}`,
1398
1500
  onDragOver: (event) => {
1399
- if (workspaceDragId === void 0) return;
1501
+ if (workspaceDragId === void 0 && !isSidebarItemDrag(event.dataTransfer) && !(zone === "pinned" && sessionDrag !== void 0)) return;
1400
1502
  event.preventDefault();
1401
1503
  event.stopPropagation();
1504
+ event.dataTransfer.dropEffect = "move";
1402
1505
  const after = event.clientY > event.currentTarget.getBoundingClientRect().top + event.currentTarget.getBoundingClientRect().height / 2;
1403
- if (zone === "pinned") {
1404
- const beforeId = dropBeforeId(pinnedGroups.map((item) => String(item.workspaceId)), String(workspace.workspaceId), after);
1405
- setPinSlot(beforeId === void 0 ? "end" : beforeId);
1406
- setWorkspaceDropId(beforeId === void 0 ? String(workspace.workspaceId) : beforeId);
1506
+ const id = String(workspace.workspaceId);
1507
+ const ids = (zone === "pinned" ? pinnedGroups : regularGroups).map((item) => String(item.workspaceId));
1508
+ const last = ids.length > 0 && ids[ids.length - 1] === id;
1509
+ if (zone === "pinned" && after && last) {
1510
+ setPinSlot("end");
1511
+ setWorkspaceDropId(void 0);
1512
+ setWorkspaceDropAfter(false);
1407
1513
  } else {
1408
- const beforeId = dropBeforeId(regularGroups.map((item) => String(item.workspaceId)), String(workspace.workspaceId), after);
1409
- setPinSlot(beforeId === void 0 ? "end" : beforeId);
1410
- setWorkspaceDropId(beforeId === void 0 ? String(workspace.workspaceId) : beforeId);
1514
+ setPinSlot(zone === "pinned" ? id : void 0);
1515
+ setWorkspaceDropId(id);
1516
+ setWorkspaceDropAfter(after);
1411
1517
  }
1412
1518
  },
1413
1519
  onDrop: (event) => {
1414
1520
  event.preventDefault();
1415
1521
  event.stopPropagation();
1416
- const dragged = workspaceDragId;
1522
+ const droppedSession = readSessionDrag(event.dataTransfer, sessionDrag?.sessionId);
1523
+ if (zone === "pinned" && droppedSession !== void 0) {
1524
+ setSessionDrag(void 0);
1525
+ setSessionDropId(void 0);
1526
+ setWorkspaceDragId(void 0);
1527
+ setWorkspaceDropId(void 0);
1528
+ setWorkspaceDropAfter(false);
1529
+ setPinSlot(void 0);
1530
+ pinSessionAt(droppedSession);
1531
+ return;
1532
+ }
1533
+ const dragged = readWorkspaceDrag(event.dataTransfer, workspaceDragId);
1417
1534
  const after = event.clientY > event.currentTarget.getBoundingClientRect().top + event.currentTarget.getBoundingClientRect().height / 2;
1418
1535
  setWorkspaceDragId(void 0);
1419
1536
  setWorkspaceDropId(void 0);
1537
+ setWorkspaceDropAfter(false);
1420
1538
  setPinSlot(void 0);
1421
1539
  if (dragged === void 0) return;
1422
1540
  if (zone === "pinned") {
@@ -1431,14 +1549,15 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1431
1549
  },
1432
1550
  children: [
1433
1551
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1434
- className: `dcu-wb-project-head${menuOpen ? " dcu-wb-menu-open" : ""}${workspaceDropId === workspace.workspaceId ? " dcu-wb-drop" : ""}`,
1552
+ 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" : ""}`,
1435
1553
  role: "treeitem",
1436
1554
  "aria-expanded": isExpanded,
1437
1555
  tabIndex: 0,
1438
1556
  draggable: true,
1439
1557
  onDragStart: (event) => {
1440
- event.dataTransfer.effectAllowed = "move";
1441
- event.dataTransfer.setData("text/plain", workspace.title);
1558
+ event.stopPropagation();
1559
+ setSessionDrag(void 0);
1560
+ writeWorkspaceDrag(event.dataTransfer, String(workspace.workspaceId), workspace.title);
1442
1561
  const preview = document.createElement("div");
1443
1562
  preview.className = "dcu-wb-drag-ghost";
1444
1563
  preview.textContent = workspace.title;
@@ -1452,10 +1571,11 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1452
1571
  onDragEnd: () => {
1453
1572
  setWorkspaceDragId(void 0);
1454
1573
  setWorkspaceDropId(void 0);
1574
+ setWorkspaceDropAfter(false);
1455
1575
  setPinSlot(void 0);
1456
1576
  },
1457
1577
  onClick: () => {
1458
- toggleGroup(workspace.workspaceId);
1578
+ if (zone !== "pinned") toggleGroup(expandKey, true);
1459
1579
  },
1460
1580
  onContextMenu: (event) => {
1461
1581
  event.preventDefault();
@@ -1484,7 +1604,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1484
1604
  onKeyDown: (event) => {
1485
1605
  if (event.key === "Enter" || event.key === " ") {
1486
1606
  event.preventDefault();
1487
- toggleGroup(workspace.workspaceId);
1607
+ if (zone !== "pinned") toggleGroup(expandKey, true);
1488
1608
  }
1489
1609
  },
1490
1610
  children: [
@@ -1507,7 +1627,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1507
1627
  top: box.top
1508
1628
  }, { immediate: true });
1509
1629
  },
1510
- children: isExpanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutline16, { size: 16 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderClose16, { size: 16 })
1630
+ children: zone !== "pinned" && isExpanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutline16, { size: 16 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderClose16, { size: 16 })
1511
1631
  }),
1512
1632
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1513
1633
  className: "dcu-wb-project-title",
@@ -1577,11 +1697,11 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1577
1697
  })
1578
1698
  ]
1579
1699
  }),
1580
- isExpanded && workspace.visibleIds.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1700
+ zone !== "pinned" && isExpanded && shownIds.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1581
1701
  className: "dcu-wb-nochat",
1582
1702
  children: t("workspace.noChat")
1583
1703
  }),
1584
- isExpanded && workspace.visibleIds.map((id) => {
1704
+ zone !== "pinned" && isExpanded && shownIds.map((id) => {
1585
1705
  const session = sessions.byId[id];
1586
1706
  if (session === void 0) return null;
1587
1707
  const path = session.cwd ?? workspace.path;
@@ -1598,7 +1718,16 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1598
1718
  draggable: true,
1599
1719
  onDragStart: (event) => {
1600
1720
  event.stopPropagation();
1601
- event.dataTransfer.effectAllowed = "move";
1721
+ setWorkspaceDragId(void 0);
1722
+ writeSessionDrag(event.dataTransfer, id, session.displayTitle);
1723
+ const preview = document.createElement("div");
1724
+ preview.className = "dcu-wb-drag-ghost";
1725
+ preview.textContent = session.displayTitle;
1726
+ document.body.appendChild(preview);
1727
+ event.dataTransfer.setDragImage(preview, 16, 18);
1728
+ window.requestAnimationFrame(() => {
1729
+ preview.remove();
1730
+ });
1602
1731
  setSessionDrag({
1603
1732
  sessionId: id,
1604
1733
  workspaceId: workspace.workspaceId
@@ -1657,7 +1786,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1657
1786
  children: [
1658
1787
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1659
1788
  className: "dcu-wb-session-title",
1660
- children: session.displayTitle
1789
+ children: session.displayTitle.split(/\r?\n/)[0] ?? session.displayTitle
1661
1790
  }),
1662
1791
  pinnedSessionIds.includes(id) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1663
1792
  className: "dcu-wb-pin",
@@ -1788,20 +1917,29 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1788
1917
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1789
1918
  className: "dcu-wb-section",
1790
1919
  "aria-label": t("workspace.pinned"),
1791
- "data-pin-over": workspaceDragId !== void 0 && pinSlot === "header",
1920
+ "data-pin-over": pinDragActive && pinSlot === "header",
1792
1921
  onDragOver: (event) => {
1793
- if (workspaceDragId === void 0) return;
1922
+ if (!pinDragActive && !isSidebarItemDrag(event.dataTransfer)) return;
1794
1923
  event.preventDefault();
1924
+ event.dataTransfer.dropEffect = "move";
1795
1925
  if (event.target === event.currentTarget || event.target instanceof Element && event.target.closest(".dcu-wb-section-head") !== null) setPinSlot("header");
1796
1926
  },
1797
1927
  onDrop: (event) => {
1798
1928
  event.preventDefault();
1799
- const dragged = workspaceDragId;
1929
+ const draggedSession = readSessionDrag(event.dataTransfer, sessionDrag?.sessionId);
1930
+ const draggedWorkspace = readWorkspaceDrag(event.dataTransfer, workspaceDragId);
1800
1931
  const slot = pinSlot;
1801
1932
  setWorkspaceDragId(void 0);
1802
1933
  setWorkspaceDropId(void 0);
1934
+ setWorkspaceDropAfter(false);
1935
+ setSessionDrag(void 0);
1936
+ setSessionDropId(void 0);
1803
1937
  setPinSlot(void 0);
1804
- if (dragged !== void 0) pinWorkspaceAt(dragged, slot === "header" ? pinnedGroups[0] === void 0 ? void 0 : String(pinnedGroups[0].workspaceId) : slot === "end" || slot === void 0 ? void 0 : slot);
1938
+ if (draggedSession !== void 0) {
1939
+ pinSessionAt(draggedSession, slot === "header" ? pinSectionSessions[0] : slot === "end" || slot === void 0 || !pinSectionSessions.includes(slot) ? void 0 : slot);
1940
+ return;
1941
+ }
1942
+ if (draggedWorkspace !== void 0) pinWorkspaceAt(draggedWorkspace, slot === "header" ? pinnedGroups[0] === void 0 ? void 0 : String(pinnedGroups[0].workspaceId) : slot === "end" || slot === void 0 || pinSectionSessions.includes(slot) ? void 0 : slot);
1805
1943
  },
1806
1944
  onDragLeave: (event) => {
1807
1945
  if (event.currentTarget.contains(event.relatedTarget)) return;
@@ -1837,18 +1975,159 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1837
1975
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1838
1976
  className: "dcu-wb-section-body",
1839
1977
  "data-open": sectionOpen("pinned"),
1840
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [pinnedGroups.length > 0 ? pinnedGroups.map((workspace) => renderGroup(workspace, "pinned")) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1841
- className: "dcu-wb-empty",
1842
- children: t("workspace.pinnedEmpty")
1843
- }), workspaceDragId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1844
- className: `dcu-wb-pin-end${pinSlot === "end" ? " dcu-wb-drop" : ""}`,
1845
- onDragOver: (event) => {
1846
- event.preventDefault();
1847
- event.stopPropagation();
1848
- setPinSlot("end");
1849
- setWorkspaceDropId(void 0);
1850
- }
1851
- })] })
1978
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1979
+ pinSectionSessions.length === 0 && pinnedGroups.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1980
+ className: "dcu-wb-empty",
1981
+ children: t("workspace.pinnedEmpty")
1982
+ }),
1983
+ pinSectionSessions.map((id) => {
1984
+ const session = sessions.byId[id];
1985
+ if (session === void 0) return null;
1986
+ const title = session.displayTitle;
1987
+ const workspace = groups.items.find((item) => item.visibleIds.includes(id));
1988
+ const path = session.cwd ?? workspace?.path;
1989
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionRow, {
1990
+ id,
1991
+ title,
1992
+ flat: true,
1993
+ selected: sessions.current === id,
1994
+ menuOpen: menu?.type === "session" && menu.id === id,
1995
+ pinned: true,
1996
+ unread: unreadSessionIds.includes(id),
1997
+ running: session.running === true,
1998
+ t,
1999
+ menuItems: sessionMenu(id, title, path),
2000
+ menuPoint: menu?.type === "session" && menu.id === id && menu.x !== void 0 && menu.y !== void 0 ? {
2001
+ x: menu.x,
2002
+ y: menu.y
2003
+ } : void 0,
2004
+ draggable: true,
2005
+ dropActive: sessionDropId === id,
2006
+ onDragStart: (event) => {
2007
+ event.stopPropagation();
2008
+ setWorkspaceDragId(void 0);
2009
+ writeSessionDrag(event.dataTransfer, id, title);
2010
+ const preview = document.createElement("div");
2011
+ preview.className = "dcu-wb-drag-ghost";
2012
+ preview.textContent = title;
2013
+ document.body.appendChild(preview);
2014
+ event.dataTransfer.setDragImage(preview, 16, 18);
2015
+ window.requestAnimationFrame(() => {
2016
+ preview.remove();
2017
+ });
2018
+ setSessionDrag({
2019
+ sessionId: id,
2020
+ workspaceId: workspace?.workspaceId ?? ""
2021
+ });
2022
+ },
2023
+ onDragEnd: () => {
2024
+ setSessionDrag(void 0);
2025
+ setSessionDropId(void 0);
2026
+ setPinSlot(void 0);
2027
+ },
2028
+ onDragOver: (event) => {
2029
+ if (sessionDrag === void 0) return;
2030
+ event.preventDefault();
2031
+ event.stopPropagation();
2032
+ setSessionDropId(id);
2033
+ setPinSlot(id);
2034
+ },
2035
+ onDrop: (event) => {
2036
+ event.preventDefault();
2037
+ event.stopPropagation();
2038
+ const draggedSession = readSessionDrag(event.dataTransfer, sessionDrag?.sessionId);
2039
+ const after = event.clientY > event.currentTarget.getBoundingClientRect().top + event.currentTarget.getBoundingClientRect().height / 2;
2040
+ setSessionDrag(void 0);
2041
+ setSessionDropId(void 0);
2042
+ setPinSlot(void 0);
2043
+ if (draggedSession === void 0) return;
2044
+ pinSessionAt(draggedSession, dropBeforeId(pinSectionSessions, id, after));
2045
+ },
2046
+ onOpen: () => {
2047
+ setUnreadSessionIds((ids) => ids.filter((item) => item !== id));
2048
+ openSession(id);
2049
+ },
2050
+ onMenuChange: (open) => {
2051
+ setMenu(open ? {
2052
+ id,
2053
+ type: "session"
2054
+ } : void 0);
2055
+ },
2056
+ onPin: () => {
2057
+ setSectionSessionIds((ids) => ids.filter((item) => item !== id));
2058
+ },
2059
+ onArchive: () => {
2060
+ run("archive", () => archiveSession(id));
2061
+ },
2062
+ onHover: (event) => {
2063
+ const box = hoverCardAnchor(event.currentTarget.getBoundingClientRect());
2064
+ showTip({
2065
+ kind: "session",
2066
+ id,
2067
+ title,
2068
+ project: workspace?.title,
2069
+ path,
2070
+ time: formatHoverTime(session.updatedAt),
2071
+ left: box.left,
2072
+ top: box.top
2073
+ });
2074
+ },
2075
+ onLeave: hideTip,
2076
+ onContextMenu: (event) => {
2077
+ event.preventDefault();
2078
+ event.stopPropagation();
2079
+ dismissTip();
2080
+ setMenu({
2081
+ id,
2082
+ type: "session",
2083
+ x: event.clientX,
2084
+ y: event.clientY
2085
+ });
2086
+ },
2087
+ onSelectAction: (action) => {
2088
+ if (busy !== void 0) return;
2089
+ if (action === "rename") beginRename("session", id, title);
2090
+ if (action === "pin") {
2091
+ setSectionSessionIds((ids) => ids.filter((item) => item !== id));
2092
+ setMenu(void 0);
2093
+ }
2094
+ if (action === "unread") {
2095
+ setUnreadSessionIds((ids) => toggleSessionId(ids, id));
2096
+ setMenu(void 0);
2097
+ }
2098
+ if (action === "archive") run("archive", () => archiveSession(id));
2099
+ if (action === "delete") {
2100
+ setDeleteTarget({
2101
+ id,
2102
+ kind: "session",
2103
+ title
2104
+ });
2105
+ setError(void 0);
2106
+ setMenu(void 0);
2107
+ }
2108
+ if (action === "fork") run("fork", () => forkSession(id));
2109
+ if (action === "openPath" && path !== void 0) run("open-path", () => openPath(path));
2110
+ if (action === "copyPath") copy(path);
2111
+ if (action === "copyTitle") copy(title);
2112
+ if (action === "copyId") copy(id);
2113
+ if (action === "copyLink") copy(sessionDeepLink(browserBase(), id));
2114
+ }
2115
+ }, id);
2116
+ }),
2117
+ pinnedGroups.map((workspace) => renderGroup(workspace, "pinned")),
2118
+ pinDragActive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2119
+ className: `dcu-wb-pin-end${pinSlot === "end" ? " dcu-wb-drop" : ""}`,
2120
+ onDragOver: (event) => {
2121
+ if (!pinDragActive && !isSidebarItemDrag(event.dataTransfer)) return;
2122
+ event.preventDefault();
2123
+ event.stopPropagation();
2124
+ event.dataTransfer.dropEffect = "move";
2125
+ setPinSlot("end");
2126
+ setWorkspaceDropId(void 0);
2127
+ setWorkspaceDropAfter(false);
2128
+ }
2129
+ })
2130
+ ] })
1852
2131
  })]
1853
2132
  }),
1854
2133
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
@@ -1954,6 +2233,29 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1954
2233
  running: session.running === true,
1955
2234
  t,
1956
2235
  menuItems: sessionMenu(id, title, session.cwd),
2236
+ draggable: true,
2237
+ onDragStart: (event) => {
2238
+ event.stopPropagation();
2239
+ setWorkspaceDragId(void 0);
2240
+ writeSessionDrag(event.dataTransfer, id, title);
2241
+ const preview = document.createElement("div");
2242
+ preview.className = "dcu-wb-drag-ghost";
2243
+ preview.textContent = title;
2244
+ document.body.appendChild(preview);
2245
+ event.dataTransfer.setDragImage(preview, 16, 18);
2246
+ window.requestAnimationFrame(() => {
2247
+ preview.remove();
2248
+ });
2249
+ setSessionDrag({
2250
+ sessionId: id,
2251
+ workspaceId: ""
2252
+ });
2253
+ },
2254
+ onDragEnd: () => {
2255
+ setSessionDrag(void 0);
2256
+ setSessionDropId(void 0);
2257
+ setPinSlot(void 0);
2258
+ },
1957
2259
  menuPoint: menu?.type === "session" && menu.id === id && menu.x !== void 0 && menu.y !== void 0 ? {
1958
2260
  x: menu.x,
1959
2261
  y: menu.y
@@ -3417,11 +3719,9 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3417
3719
  });
3418
3720
  }
3419
3721
  CodexSidebar.displayName = "michengai-codex-ui";
3722
+ //#endregion
3723
+ //#region src/dependencies.ts
3420
3724
  const MANAGED_DEPENDENCIES = [
3421
- {
3422
- id: "suite",
3423
- packageName: "@michengai/dsh-codex-suite"
3424
- },
3425
3725
  {
3426
3726
  id: "ui",
3427
3727
  packageName: "@michengai/dsh-codex-ui"
@@ -3711,7 +4011,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3711
4011
  "search.actions": "快捷操作",
3712
4012
  "workspace.label": "工作区会话",
3713
4013
  "workspace.pinned": "置顶",
3714
- "workspace.pinnedEmpty": "拖动项目到此处置顶",
4014
+ "workspace.pinnedEmpty": "拖动项目或会话到此处置顶",
3715
4015
  "workspace.projects": "项目",
3716
4016
  "workspace.recent": "最近",
3717
4017
  "workspace.recentEmpty": "无聊天",
@@ -3782,7 +4082,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3782
4082
  "about.feature.conversation": "保留原生消息、工具调用、输入、权限和模型选择,仅调整容器视觉",
3783
4083
  "about.feature.navigator": "当前会话提供轮次缩略导航,可快速跳转至每一次用户提问",
3784
4084
  "about.dependencies": "配套管理插件",
3785
- "about.dependenciesDescription": "可先安装 Codex 套件一键装齐配套插件,也可单独检查并更新专家、技能、归档、IM、定时任务和第三方插件市场。",
4085
+ "about.dependenciesDescription": "每个配套插件都单独安装和更新,互不影响。可分别检查 Codex UI、专家、技能、归档、IM、定时任务和第三方插件市场。",
3786
4086
  "about.loading": "正在读取依赖安装状态…",
3787
4087
  "about.statusFailed": "暂时无法读取依赖安装状态。",
3788
4088
  "about.installed": "已安装",
@@ -3792,7 +4092,8 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3792
4092
  "about.update": "更新",
3793
4093
  "about.installing": "安装中",
3794
4094
  "about.installFailed": "依赖安装失败,请稍后重试。",
3795
- "about.restartRequired": "依赖安装完成。请重启 DSH Web 服务以加载新插件。",
4095
+ "about.restartRequired": "正在热更新插件,窗口会自动刷新。",
4096
+ "about.dependency.dsh": "DeepSeek Harness",
3796
4097
  "about.dependency.suite": "Codex 套件",
3797
4098
  "about.dependency.ui": "Codex UI",
3798
4099
  "about.dependency.experts": "专家管理",
@@ -3842,7 +4143,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3842
4143
  "workspace.pin": "Pin project",
3843
4144
  "workspace.unpin": "Unpin",
3844
4145
  "workspace.pinned": "Pinned",
3845
- "workspace.pinnedEmpty": "Drag a project here to pin it",
4146
+ "workspace.pinnedEmpty": "Drag a project or chat here to pin it",
3846
4147
  "workspace.openPath": "Open in file explorer",
3847
4148
  "workspace.delete": "Delete project",
3848
4149
  "workspace.deleteDescription": "This removes “{name}” from the project list. The folder and conversation records remain.",
@@ -3904,7 +4205,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3904
4205
  "about.feature.conversation": "Keeps native messages, tool calls, composer, permissions, and model selection while refining the container visuals",
3905
4206
  "about.feature.navigator": "Navigate directly to each user prompt with the current conversation turn navigator",
3906
4207
  "about.dependencies": "Companion management plugins",
3907
- "about.dependenciesDescription": "Install the Codex suite to get all companion plugins at once, or check and update the expert, skill, archive, IM, scheduled-task, and marketplace plugins individually.",
4208
+ "about.dependenciesDescription": "Each companion plugin is installed and updated separately. Check Codex UI, expert, skill, archive, IM, scheduled-task, and marketplace plugins on their own.",
3908
4209
  "about.loading": "Loading dependency status…",
3909
4210
  "about.statusFailed": "Dependency status is temporarily unavailable.",
3910
4211
  "about.installed": "Installed",
@@ -3914,7 +4215,8 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3914
4215
  "about.update": "Update",
3915
4216
  "about.installing": "Installing",
3916
4217
  "about.installFailed": "Dependency installation failed. Try again later.",
3917
- "about.restartRequired": "Installation is complete. Restart DSH Web to load the new plugin.",
4218
+ "about.restartRequired": "Updating plugins now. The window will refresh automatically.",
4219
+ "about.dependency.dsh": "DeepSeek Harness",
3918
4220
  "about.dependency.suite": "Codex suite",
3919
4221
  "about.dependency.ui": "Codex UI",
3920
4222
  "about.dependency.experts": "Expert management",
package/dist/index.mjs CHANGED
@@ -14,10 +14,6 @@ const SUITE_MEMBER_PACKAGES = [
14
14
  "@michengai/dsh-automation"
15
15
  ];
16
16
  const MANAGED_DEPENDENCIES = [
17
- {
18
- id: "suite",
19
- packageName: SUITE_PACKAGE
20
- },
21
17
  {
22
18
  id: "ui",
23
19
  packageName: "@michengai/dsh-codex-ui"
@@ -52,6 +48,8 @@ function managedDependency(id) {
52
48
  }
53
49
  //#endregion
54
50
  //#region src/dependency-manager.ts
51
+ const PROFILE_PENDING_UPDATES_FILE = ".dsh-pending-updates.json";
52
+ const APPLY_PLUGIN_UPDATES_IPC = "apply-plugin-updates";
55
53
  function profileDirectory() {
56
54
  if (process.env.DSH_PROFILE_DIR !== void 0) return process.env.DSH_PROFILE_DIR;
57
55
  return resolve(homedir(), ".dsh", "profiles", "web");
@@ -68,14 +66,14 @@ async function declaredPluginNames() {
68
66
  throw error;
69
67
  }
70
68
  }
71
- /** 安装套件前卸掉已单独安装的子插件,避免两套 patch 重复注册同一 id。 */
69
+ /** 单独更新子插件时先卸套件,避免两套 patch 冲突。 */
72
70
  function pluginsToRemoveBeforeInstall(declared, installing) {
73
- if (installing !== "@michengai/dsh-codex-suite") return [];
74
- return SUITE_MEMBER_PACKAGES.filter((name) => declared.includes(name));
71
+ if (SUITE_MEMBER_PACKAGES.includes(installing) && declared.includes("@michengai/dsh-codex-suite")) return [SUITE_PACKAGE];
72
+ if (installing === "@michengai/dsh-codex-suite") return SUITE_MEMBER_PACKAGES.filter((name) => declared.includes(name));
73
+ return [];
75
74
  }
76
- /** 已装套件时,子插件的安装/更新改走套件,避免再写入一份 bundle。 */
77
- function resolveDshPluginTarget(installing, declared) {
78
- if (installing !== "@michengai/dsh-codex-suite" && SUITE_MEMBER_PACKAGES.includes(installing) && declared.includes("@michengai/dsh-codex-suite")) return SUITE_PACKAGE;
75
+ /** 点击哪个包就更新哪个包,不再把子插件重定向到套件。 */
76
+ function resolveDshPluginTarget(installing, _declared = []) {
79
77
  return installing;
80
78
  }
81
79
  async function installedPackageVersion(packageName) {
@@ -190,12 +188,55 @@ function resolveDshCliEntry(entry = process.argv[1], cwd = process.cwd()) {
190
188
  if (entry.startsWith("file:")) return fileURLToPath(entry);
191
189
  return resolve(cwd, entry);
192
190
  }
191
+ function requestDesktopHotUpdate(send = process.send) {
192
+ if (typeof send !== "function") return false;
193
+ send(APPLY_PLUGIN_UPDATES_IPC);
194
+ return true;
195
+ }
196
+ function isRestartableInstallError(error) {
197
+ const message = error instanceof Error ? error.message : String(error);
198
+ return /完全退出桌面端|正在运行的插件|pnpm 仓库不一致/.test(message);
199
+ }
200
+ async function recordPendingUpdate(packageName, version) {
201
+ const pendingPath = resolve(profileDirectory(), PROFILE_PENDING_UPDATES_FILE);
202
+ let packages = [];
203
+ try {
204
+ const parsed = JSON.parse(await readFile(pendingPath, "utf8"));
205
+ if (Array.isArray(parsed.packages)) packages = parsed.packages.flatMap((item) => {
206
+ if (item === null || typeof item !== "object") return [];
207
+ const record = item;
208
+ if (typeof record.packageName !== "string" || typeof record.version !== "string") return [];
209
+ return [{
210
+ packageName: record.packageName,
211
+ version: record.version
212
+ }];
213
+ });
214
+ } catch (error) {
215
+ if (error.code !== "ENOENT") throw error;
216
+ }
217
+ packages = packages.filter((item) => item.packageName !== packageName);
218
+ packages.push({
219
+ packageName,
220
+ version
221
+ });
222
+ await writeFile(pendingPath, `${JSON.stringify({ packages }, void 0, 2)}\n`, "utf8");
223
+ }
224
+ async function recordDeclaredVersion(packageName, version) {
225
+ const manifestPath = resolve(profileDirectory(), "package.json");
226
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
227
+ manifest.dependencies = {
228
+ ...manifest.dependencies,
229
+ [packageName]: version
230
+ };
231
+ await writeFile(manifestPath, `${JSON.stringify(manifest, void 0, 2)}\n`, "utf8");
232
+ }
193
233
  function pluginCommandError(stderr) {
194
234
  const detail = stderr.replace(/\s+/g, " ").trim();
195
235
  if (detail.includes("minimumReleaseAge") || detail.includes("Release age")) return /* @__PURE__ */ new Error("更新被 pnpm 发布时间保护拦截。请确认已写入当前版本白名单后重试。");
196
- if (detail.includes("EPERM") || detail.includes("EBUSY") || detail.includes("EACCES")) return /* @__PURE__ */ new Error("无法覆盖正在运行的插件文件。请先停止 DSH Web,再点击更新。");
197
- if (detail.includes("NO_MATCHING_VERSION") || detail.includes("No matching version")) return /* @__PURE__ */ new Error("当前 npm 镜像还没有这个版本。请稍后重试,或改用官方源安装。");
198
- return /* @__PURE__ */ new Error(" npm 安装或更新依赖失败。请检查网络、npm registry 或发布时间保护后重试。");
236
+ if (/EPERM|EBUSY|EACCES|unable to unlink|ERR_PNPM_LOCKED|Lock/i.test(detail)) return /* @__PURE__ */ new Error("无法覆盖正在运行的插件文件。请先完全退出桌面端,再重新打开后更新。");
237
+ if (/UNEXPECTED_STORE|Unexpected store location/i.test(detail)) return /* @__PURE__ */ new Error("插件目录和 pnpm 仓库不一致。请完全退出桌面端后再更新。");
238
+ if (/pnpm not found/i.test(detail)) return /* @__PURE__ */ new Error("当前环境找不到 pnpm。请从桌面端启动后再更新。");
239
+ return /* @__PURE__ */ new Error("无法在应用运行时更新插件。请先完全退出桌面端,再重新打开后更新。");
199
240
  }
200
241
  /**
201
242
  * 复用启动当前服务的 DSH CLI:它会通过 pnpm 从 npm 安装或更新,并自动维护
@@ -220,19 +261,21 @@ function runDshPlugin(args) {
220
261
  windowsHide: true,
221
262
  stdio: [
222
263
  "ignore",
223
- "ignore",
264
+ "pipe",
224
265
  "pipe"
225
266
  ]
226
267
  });
227
- let stderr = "";
228
- child.stderr?.on("data", (chunk) => {
229
- stderr += String(chunk);
230
- });
268
+ let output = "";
269
+ const collect = (chunk) => {
270
+ output += String(chunk);
271
+ };
272
+ child.stdout?.on("data", collect);
273
+ child.stderr?.on("data", collect);
231
274
  child.once("error", () => {
232
275
  reject(/* @__PURE__ */ new Error("无法启动 DSH 插件安装命令。请确认 Node.js 与 pnpm 可用后重试。"));
233
276
  });
234
277
  child.once("exit", (code) => {
235
- code === 0 ? resolvePromise() : reject(pluginCommandError(stderr));
278
+ code === 0 ? resolvePromise() : reject(pluginCommandError(output));
236
279
  });
237
280
  });
238
281
  }
@@ -260,13 +303,20 @@ async function installDependencyLocked(id) {
260
303
  const remove = pluginsToRemoveBeforeInstall(declared, target);
261
304
  if (remove.length > 0) await runDshPlugin(["remove", ...remove]);
262
305
  await ensureLatestReleaseAllowed(target, targetVersion);
263
- await runDshPlugin([
264
- "add",
265
- `${target}@${targetVersion}`,
266
- "--registry=https://registry.npmjs.org/"
267
- ]);
268
- const installed = await installedPackageVersion(target);
269
- if (installed !== targetVersion) throw new Error(`已请求 ${target}@${targetVersion},但当前仍是 ${installed ?? "未安装"}。请先停止 DSH Web 后再更新。`);
306
+ await recordDeclaredVersion(target, targetVersion);
307
+ await recordPendingUpdate(target, targetVersion);
308
+ if (requestDesktopHotUpdate()) return dependencyStatuses();
309
+ try {
310
+ await runDshPlugin([
311
+ "add",
312
+ `${target}@${targetVersion}`,
313
+ "--registry=https://registry.npmjs.org/"
314
+ ]);
315
+ } catch (error) {
316
+ if (isRestartableInstallError(error)) return dependencyStatuses();
317
+ throw error;
318
+ }
319
+ if (await installedPackageVersion(target) !== targetVersion) return dependencyStatuses();
270
320
  return dependencyStatuses();
271
321
  }
272
322
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-codex-ui",
3
- "version": "0.2.58",
3
+ "version": "0.2.60",
4
4
  "description": "以 Codex 风格重构 DSH Web 侧栏的独立客户端插件",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
@@ -51,6 +51,10 @@
51
51
  "platform": "web"
52
52
  }
53
53
  },
54
+ "scripts": {
55
+ "build": "tsc --noEmit && tsdown",
56
+ "test": "tsx tests/permission-i18n.assert.ts && 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"
57
+ },
54
58
  "peerDependencies": {
55
59
  "@deepseek-ai/cordis": ">=4.0.1 <5.0.0",
56
60
  "@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.0 <0.2.0",
@@ -96,9 +100,5 @@
96
100
  "tsx": "^4.22.4",
97
101
  "typescript": "^6.0.3",
98
102
  "vitest": "^4.1.8"
99
- },
100
- "scripts": {
101
- "build": "tsc --noEmit && tsdown",
102
- "test": "tsx tests/permission-i18n.assert.ts && 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"
103
103
  }
104
- }
104
+ }