@michengai/dsh-codex-ui 0.2.59 → 0.2.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/client.js +395 -60
  2. package/dist/index.mjs +119 -39
  3. package/package.json +6 -6
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,.dcu-wb-pin-start{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-pin-start.dcu-wb-drop::before{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,28 @@ 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",
1792
1920
  onDragOver: (event) => {
1793
- if (workspaceDragId === void 0) return;
1921
+ if (!pinDragActive && !isSidebarItemDrag(event.dataTransfer)) return;
1794
1922
  event.preventDefault();
1923
+ event.dataTransfer.dropEffect = "move";
1795
1924
  if (event.target === event.currentTarget || event.target instanceof Element && event.target.closest(".dcu-wb-section-head") !== null) setPinSlot("header");
1796
1925
  },
1797
1926
  onDrop: (event) => {
1798
1927
  event.preventDefault();
1799
- const dragged = workspaceDragId;
1928
+ const draggedSession = readSessionDrag(event.dataTransfer, sessionDrag?.sessionId);
1929
+ const draggedWorkspace = readWorkspaceDrag(event.dataTransfer, workspaceDragId);
1800
1930
  const slot = pinSlot;
1801
1931
  setWorkspaceDragId(void 0);
1802
1932
  setWorkspaceDropId(void 0);
1933
+ setWorkspaceDropAfter(false);
1934
+ setSessionDrag(void 0);
1935
+ setSessionDropId(void 0);
1803
1936
  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);
1937
+ if (draggedSession !== void 0) {
1938
+ pinSessionAt(draggedSession, slot === "header" ? pinSectionSessions[0] : slot === "end" || slot === void 0 || !pinSectionSessions.includes(slot) ? void 0 : slot);
1939
+ return;
1940
+ }
1941
+ 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
1942
  },
1806
1943
  onDragLeave: (event) => {
1807
1944
  if (event.currentTarget.contains(event.relatedTarget)) return;
@@ -1837,18 +1974,171 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1837
1974
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1838
1975
  className: "dcu-wb-section-body",
1839
1976
  "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
- })] })
1977
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1978
+ (pinDragActive || pinSlot === "header") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1979
+ className: `dcu-wb-pin-start${pinSlot === "header" ? " dcu-wb-drop" : ""}`,
1980
+ onDragOver: (event) => {
1981
+ if (!pinDragActive && !isSidebarItemDrag(event.dataTransfer)) return;
1982
+ event.preventDefault();
1983
+ event.stopPropagation();
1984
+ event.dataTransfer.dropEffect = "move";
1985
+ setPinSlot("header");
1986
+ setWorkspaceDropId(void 0);
1987
+ setWorkspaceDropAfter(false);
1988
+ }
1989
+ }),
1990
+ pinSectionSessions.length === 0 && pinnedGroups.length === 0 && pinSlot !== "header" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1991
+ className: "dcu-wb-empty",
1992
+ children: t("workspace.pinnedEmpty")
1993
+ }),
1994
+ pinSectionSessions.map((id) => {
1995
+ const session = sessions.byId[id];
1996
+ if (session === void 0) return null;
1997
+ const title = session.displayTitle;
1998
+ const workspace = groups.items.find((item) => item.visibleIds.includes(id));
1999
+ const path = session.cwd ?? workspace?.path;
2000
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionRow, {
2001
+ id,
2002
+ title,
2003
+ flat: true,
2004
+ selected: sessions.current === id,
2005
+ menuOpen: menu?.type === "session" && menu.id === id,
2006
+ pinned: true,
2007
+ unread: unreadSessionIds.includes(id),
2008
+ running: session.running === true,
2009
+ t,
2010
+ menuItems: sessionMenu(id, title, path),
2011
+ menuPoint: menu?.type === "session" && menu.id === id && menu.x !== void 0 && menu.y !== void 0 ? {
2012
+ x: menu.x,
2013
+ y: menu.y
2014
+ } : void 0,
2015
+ draggable: true,
2016
+ dropActive: sessionDropId === id,
2017
+ onDragStart: (event) => {
2018
+ event.stopPropagation();
2019
+ setWorkspaceDragId(void 0);
2020
+ writeSessionDrag(event.dataTransfer, id, title);
2021
+ const preview = document.createElement("div");
2022
+ preview.className = "dcu-wb-drag-ghost";
2023
+ preview.textContent = title;
2024
+ document.body.appendChild(preview);
2025
+ event.dataTransfer.setDragImage(preview, 16, 18);
2026
+ window.requestAnimationFrame(() => {
2027
+ preview.remove();
2028
+ });
2029
+ setSessionDrag({
2030
+ sessionId: id,
2031
+ workspaceId: workspace?.workspaceId ?? ""
2032
+ });
2033
+ },
2034
+ onDragEnd: () => {
2035
+ setSessionDrag(void 0);
2036
+ setSessionDropId(void 0);
2037
+ setPinSlot(void 0);
2038
+ },
2039
+ onDragOver: (event) => {
2040
+ if (sessionDrag === void 0) return;
2041
+ event.preventDefault();
2042
+ event.stopPropagation();
2043
+ setSessionDropId(id);
2044
+ setPinSlot(id);
2045
+ },
2046
+ onDrop: (event) => {
2047
+ event.preventDefault();
2048
+ event.stopPropagation();
2049
+ const draggedSession = readSessionDrag(event.dataTransfer, sessionDrag?.sessionId);
2050
+ const after = event.clientY > event.currentTarget.getBoundingClientRect().top + event.currentTarget.getBoundingClientRect().height / 2;
2051
+ setSessionDrag(void 0);
2052
+ setSessionDropId(void 0);
2053
+ setPinSlot(void 0);
2054
+ if (draggedSession === void 0) return;
2055
+ pinSessionAt(draggedSession, dropBeforeId(pinSectionSessions, id, after));
2056
+ },
2057
+ onOpen: () => {
2058
+ setUnreadSessionIds((ids) => ids.filter((item) => item !== id));
2059
+ openSession(id);
2060
+ },
2061
+ onMenuChange: (open) => {
2062
+ setMenu(open ? {
2063
+ id,
2064
+ type: "session"
2065
+ } : void 0);
2066
+ },
2067
+ onPin: () => {
2068
+ setSectionSessionIds((ids) => ids.filter((item) => item !== id));
2069
+ },
2070
+ onArchive: () => {
2071
+ run("archive", () => archiveSession(id));
2072
+ },
2073
+ onHover: (event) => {
2074
+ const box = hoverCardAnchor(event.currentTarget.getBoundingClientRect());
2075
+ showTip({
2076
+ kind: "session",
2077
+ id,
2078
+ title,
2079
+ project: workspace?.title,
2080
+ path,
2081
+ time: formatHoverTime(session.updatedAt),
2082
+ left: box.left,
2083
+ top: box.top
2084
+ });
2085
+ },
2086
+ onLeave: hideTip,
2087
+ onContextMenu: (event) => {
2088
+ event.preventDefault();
2089
+ event.stopPropagation();
2090
+ dismissTip();
2091
+ setMenu({
2092
+ id,
2093
+ type: "session",
2094
+ x: event.clientX,
2095
+ y: event.clientY
2096
+ });
2097
+ },
2098
+ onSelectAction: (action) => {
2099
+ if (busy !== void 0) return;
2100
+ if (action === "rename") beginRename("session", id, title);
2101
+ if (action === "pin") {
2102
+ setSectionSessionIds((ids) => ids.filter((item) => item !== id));
2103
+ setMenu(void 0);
2104
+ }
2105
+ if (action === "unread") {
2106
+ setUnreadSessionIds((ids) => toggleSessionId(ids, id));
2107
+ setMenu(void 0);
2108
+ }
2109
+ if (action === "archive") run("archive", () => archiveSession(id));
2110
+ if (action === "delete") {
2111
+ setDeleteTarget({
2112
+ id,
2113
+ kind: "session",
2114
+ title
2115
+ });
2116
+ setError(void 0);
2117
+ setMenu(void 0);
2118
+ }
2119
+ if (action === "fork") run("fork", () => forkSession(id));
2120
+ if (action === "openPath" && path !== void 0) run("open-path", () => openPath(path));
2121
+ if (action === "copyPath") copy(path);
2122
+ if (action === "copyTitle") copy(title);
2123
+ if (action === "copyId") copy(id);
2124
+ if (action === "copyLink") copy(sessionDeepLink(browserBase(), id));
2125
+ }
2126
+ }, id);
2127
+ }),
2128
+ pinnedGroups.map((workspace) => renderGroup(workspace, "pinned")),
2129
+ pinDragActive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2130
+ className: `dcu-wb-pin-end${pinSlot === "end" ? " dcu-wb-drop" : ""}`,
2131
+ onDragOver: (event) => {
2132
+ if (!pinDragActive && !isSidebarItemDrag(event.dataTransfer)) return;
2133
+ event.preventDefault();
2134
+ event.stopPropagation();
2135
+ event.dataTransfer.dropEffect = "move";
2136
+ setPinSlot("end");
2137
+ setWorkspaceDropId(void 0);
2138
+ setWorkspaceDropAfter(false);
2139
+ }
2140
+ })
2141
+ ] })
1852
2142
  })]
1853
2143
  }),
1854
2144
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
@@ -1954,6 +2244,29 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
1954
2244
  running: session.running === true,
1955
2245
  t,
1956
2246
  menuItems: sessionMenu(id, title, session.cwd),
2247
+ draggable: true,
2248
+ onDragStart: (event) => {
2249
+ event.stopPropagation();
2250
+ setWorkspaceDragId(void 0);
2251
+ writeSessionDrag(event.dataTransfer, id, title);
2252
+ const preview = document.createElement("div");
2253
+ preview.className = "dcu-wb-drag-ghost";
2254
+ preview.textContent = title;
2255
+ document.body.appendChild(preview);
2256
+ event.dataTransfer.setDragImage(preview, 16, 18);
2257
+ window.requestAnimationFrame(() => {
2258
+ preview.remove();
2259
+ });
2260
+ setSessionDrag({
2261
+ sessionId: id,
2262
+ workspaceId: ""
2263
+ });
2264
+ },
2265
+ onDragEnd: () => {
2266
+ setSessionDrag(void 0);
2267
+ setSessionDropId(void 0);
2268
+ setPinSlot(void 0);
2269
+ },
1957
2270
  menuPoint: menu?.type === "session" && menu.id === id && menu.x !== void 0 && menu.y !== void 0 ? {
1958
2271
  x: menu.x,
1959
2272
  y: menu.y
@@ -3417,10 +3730,12 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3417
3730
  });
3418
3731
  }
3419
3732
  CodexSidebar.displayName = "michengai-codex-ui";
3733
+ //#endregion
3734
+ //#region src/dependencies.ts
3420
3735
  const MANAGED_DEPENDENCIES = [
3421
3736
  {
3422
- id: "suite",
3423
- packageName: "@michengai/dsh-codex-suite"
3737
+ id: "dsh",
3738
+ packageName: "@deepseek-ai/dsh"
3424
3739
  },
3425
3740
  {
3426
3741
  id: "ui",
@@ -3469,31 +3784,48 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3469
3784
  const [installing, setInstalling] = (0, react.useState)();
3470
3785
  const [message, setMessage] = (0, react.useState)();
3471
3786
  const alive = (0, react.useRef)(true);
3787
+ const root = (0, react.useRef)(null);
3788
+ const stateRef = (0, react.useRef)("loading");
3789
+ const requestId = (0, react.useRef)(0);
3790
+ stateRef.current = state;
3472
3791
  (0, react.useEffect)(() => () => {
3473
3792
  alive.current = false;
3474
3793
  }, []);
3475
3794
  const load = (0, react.useCallback)(async (signal) => {
3476
- setState("loading");
3795
+ const currentRequest = ++requestId.current;
3796
+ if (stateRef.current !== "ready") setState("loading");
3477
3797
  try {
3478
3798
  const response = await fetch(endpoint, {
3479
3799
  cache: "no-store",
3480
3800
  signal
3481
3801
  });
3482
3802
  const payload = await response.json();
3483
- if (signal?.aborted) return;
3803
+ if (signal?.aborted || currentRequest !== requestId.current) return;
3484
3804
  if (!response.ok || !Array.isArray(payload.dependencies) || !payload.dependencies.every(isDependencyStatus)) throw new Error();
3485
3805
  setDependencies(payload.dependencies);
3486
3806
  setState("ready");
3487
3807
  } catch (error) {
3488
- if (signal?.aborted || error instanceof DOMException && error.name === "AbortError") return;
3489
- setState("failed");
3808
+ if (signal?.aborted || error instanceof DOMException && error.name === "AbortError" || currentRequest !== requestId.current) return;
3809
+ if (stateRef.current !== "ready") setState("failed");
3490
3810
  }
3491
3811
  }, []);
3492
3812
  (0, react.useEffect)(() => {
3813
+ const node = root.current;
3493
3814
  const controller = new AbortController();
3494
- load(controller.signal);
3815
+ const refresh = () => {
3816
+ load(controller.signal);
3817
+ };
3818
+ refresh();
3819
+ if (node === null || typeof IntersectionObserver === "undefined") return () => {
3820
+ controller.abort();
3821
+ };
3822
+ const observer = new IntersectionObserver((entries) => {
3823
+ if (entries.some((entry) => entry.isIntersecting)) refresh();
3824
+ }, { threshold: .2 });
3825
+ observer.observe(node);
3495
3826
  return () => {
3496
3827
  controller.abort();
3828
+ observer.disconnect();
3497
3829
  };
3498
3830
  }, [load]);
3499
3831
  const install = async (id) => {
@@ -3522,6 +3854,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3522
3854
  };
3523
3855
  const title = (id) => t(`about.dependency.${id}`);
3524
3856
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3857
+ ref: root,
3525
3858
  className: "dcu-about",
3526
3859
  "aria-label": t("about.nav"),
3527
3860
  children: [
@@ -3711,7 +4044,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3711
4044
  "search.actions": "快捷操作",
3712
4045
  "workspace.label": "工作区会话",
3713
4046
  "workspace.pinned": "置顶",
3714
- "workspace.pinnedEmpty": "拖动项目到此处置顶",
4047
+ "workspace.pinnedEmpty": "拖动项目或会话到此处置顶",
3715
4048
  "workspace.projects": "项目",
3716
4049
  "workspace.recent": "最近",
3717
4050
  "workspace.recentEmpty": "无聊天",
@@ -3782,7 +4115,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3782
4115
  "about.feature.conversation": "保留原生消息、工具调用、输入、权限和模型选择,仅调整容器视觉",
3783
4116
  "about.feature.navigator": "当前会话提供轮次缩略导航,可快速跳转至每一次用户提问",
3784
4117
  "about.dependencies": "配套管理插件",
3785
- "about.dependenciesDescription": "可先安装 Codex 套件一键装齐配套插件,也可单独检查并更新专家、技能、归档、IM、定时任务和第三方插件市场。",
4118
+ "about.dependenciesDescription": "每个配套插件都单独安装和更新,互不影响。可分别检查 Codex UI、专家、技能、归档、IM、定时任务和第三方插件市场。",
3786
4119
  "about.loading": "正在读取依赖安装状态…",
3787
4120
  "about.statusFailed": "暂时无法读取依赖安装状态。",
3788
4121
  "about.installed": "已安装",
@@ -3792,7 +4125,8 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3792
4125
  "about.update": "更新",
3793
4126
  "about.installing": "安装中",
3794
4127
  "about.installFailed": "依赖安装失败,请稍后重试。",
3795
- "about.restartRequired": "依赖安装完成。请重启 DSH Web 服务以加载新插件。",
4128
+ "about.restartRequired": "正在热更新插件,窗口会自动刷新。",
4129
+ "about.dependency.dsh": "DeepSeek Harness",
3796
4130
  "about.dependency.suite": "Codex 套件",
3797
4131
  "about.dependency.ui": "Codex UI",
3798
4132
  "about.dependency.experts": "专家管理",
@@ -3842,7 +4176,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3842
4176
  "workspace.pin": "Pin project",
3843
4177
  "workspace.unpin": "Unpin",
3844
4178
  "workspace.pinned": "Pinned",
3845
- "workspace.pinnedEmpty": "Drag a project here to pin it",
4179
+ "workspace.pinnedEmpty": "Drag a project or chat here to pin it",
3846
4180
  "workspace.openPath": "Open in file explorer",
3847
4181
  "workspace.delete": "Delete project",
3848
4182
  "workspace.deleteDescription": "This removes “{name}” from the project list. The folder and conversation records remain.",
@@ -3904,7 +4238,7 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3904
4238
  "about.feature.conversation": "Keeps native messages, tool calls, composer, permissions, and model selection while refining the container visuals",
3905
4239
  "about.feature.navigator": "Navigate directly to each user prompt with the current conversation turn navigator",
3906
4240
  "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.",
4241
+ "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
4242
  "about.loading": "Loading dependency status…",
3909
4243
  "about.statusFailed": "Dependency status is temporarily unavailable.",
3910
4244
  "about.installed": "Installed",
@@ -3914,7 +4248,8 @@ header [data-dcu-title-folder] svg,header [data-dcu-title-more] svg{display:bloc
3914
4248
  "about.update": "Update",
3915
4249
  "about.installing": "Installing",
3916
4250
  "about.installFailed": "Dependency installation failed. Try again later.",
3917
- "about.restartRequired": "Installation is complete. Restart DSH Web to load the new plugin.",
4251
+ "about.restartRequired": "Updating plugins now. The window will refresh automatically.",
4252
+ "about.dependency.dsh": "DeepSeek Harness",
3918
4253
  "about.dependency.suite": "Codex suite",
3919
4254
  "about.dependency.ui": "Codex UI",
3920
4255
  "about.dependency.experts": "Expert management",
package/dist/index.mjs CHANGED
@@ -15,8 +15,8 @@ const SUITE_MEMBER_PACKAGES = [
15
15
  ];
16
16
  const MANAGED_DEPENDENCIES = [
17
17
  {
18
- id: "suite",
19
- packageName: SUITE_PACKAGE
18
+ id: "dsh",
19
+ packageName: "@deepseek-ai/dsh"
20
20
  },
21
21
  {
22
22
  id: "ui",
@@ -52,6 +52,8 @@ function managedDependency(id) {
52
52
  }
53
53
  //#endregion
54
54
  //#region src/dependency-manager.ts
55
+ const PROFILE_PENDING_UPDATES_FILE = ".dsh-pending-updates.json";
56
+ const APPLY_PLUGIN_UPDATES_IPC = "apply-plugin-updates";
55
57
  function profileDirectory() {
56
58
  if (process.env.DSH_PROFILE_DIR !== void 0) return process.env.DSH_PROFILE_DIR;
57
59
  return resolve(homedir(), ".dsh", "profiles", "web");
@@ -59,50 +61,60 @@ function profileDirectory() {
59
61
  async function declaredPluginNames() {
60
62
  try {
61
63
  const manifest = JSON.parse(await readFile(resolve(profileDirectory(), "package.json"), "utf8"));
62
- return Object.keys({
64
+ return [.../* @__PURE__ */ new Set([...Object.keys({
63
65
  ...manifest.devDependencies,
64
66
  ...manifest.dependencies
65
- });
67
+ }), ...manifest.dsh?.profile?.bundles ?? []])];
66
68
  } catch (error) {
67
69
  if (error.code === "ENOENT") return [];
68
70
  throw error;
69
71
  }
70
72
  }
71
- /** 安装套件前卸掉已单独安装的子插件,避免两套 patch 重复注册同一 id。 */
73
+ /** 单独更新子插件时先卸套件,避免两套 patch 冲突。 */
72
74
  function pluginsToRemoveBeforeInstall(declared, installing) {
73
- if (installing !== "@michengai/dsh-codex-suite") return [];
74
- return SUITE_MEMBER_PACKAGES.filter((name) => declared.includes(name));
75
+ if (SUITE_MEMBER_PACKAGES.includes(installing) && declared.includes("@michengai/dsh-codex-suite")) return [SUITE_PACKAGE];
76
+ if (installing === "@michengai/dsh-codex-suite") return SUITE_MEMBER_PACKAGES.filter((name) => declared.includes(name));
77
+ return [];
75
78
  }
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;
79
+ /** 点击哪个包就更新哪个包,不再把子插件重定向到套件。 */
80
+ function resolveDshPluginTarget(installing, _declared = []) {
79
81
  return installing;
80
82
  }
83
+ function isOfficialRuntimePackage(packageName) {
84
+ return packageName === "@deepseek-ai/dsh" || packageName.startsWith("@deepseek-ai/dsh-");
85
+ }
86
+ function packageLookupRoots(packageName) {
87
+ if (isOfficialRuntimePackage(packageName) && process.env.DSH_RUNTIME_DIR !== void 0 && process.env.DSH_RUNTIME_DIR !== "") return [process.env.DSH_RUNTIME_DIR, profileDirectory()];
88
+ return [profileDirectory()];
89
+ }
81
90
  async function installedPackageVersion(packageName) {
82
- try {
83
- const manifest = JSON.parse(await readFile(resolve(profileDirectory(), "node_modules", ...packageName.split("/"), "package.json"), "utf8"));
84
- return typeof manifest.version === "string" ? manifest.version : void 0;
91
+ for (const root of packageLookupRoots(packageName)) try {
92
+ const manifest = JSON.parse(await readFile(resolve(root, "node_modules", ...packageName.split("/"), "package.json"), "utf8"));
93
+ if (typeof manifest.version === "string" && manifest.version !== "") return manifest.version;
85
94
  } catch (error) {
86
- if (error.code === "ENOENT") return void 0;
87
- throw error;
95
+ if (error.code !== "ENOENT") throw error;
88
96
  }
89
97
  }
90
- /** node_modules 实际版本为准;套件嵌套安装没有写进顶层 dependencies,也算已安装。 */
98
+ /** 磁盘有包且仍在 profile 声明里,才算已安装。卸载后残留的 node_modules 不算。 */
91
99
  function isManagedPackageInstalled(input) {
92
- return input.installedVersion !== void 0 && input.installedVersion !== "";
100
+ return input.declared && input.installedVersion !== void 0 && input.installedVersion !== "";
93
101
  }
94
102
  /** npm latest 查询缓存有效期:避免每次打开“关于”页都打 7 个 registry 请求。 */
95
103
  const LATEST_CACHE_TTL_MS = 3e5;
96
104
  const latestCache = /* @__PURE__ */ new Map();
97
- async function npmLatestVersion(packageName) {
98
- const hit = latestCache.get(packageName);
105
+ function cacheKey(packageName, tag) {
106
+ return packageName + "@" + tag;
107
+ }
108
+ async function npmTaggedVersion(packageName, tag) {
109
+ const key = cacheKey(packageName, tag);
110
+ const hit = latestCache.get(key);
99
111
  if (hit !== void 0 && Date.now() - hit.at < LATEST_CACHE_TTL_MS) return hit.version;
100
112
  try {
101
- const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, { signal: AbortSignal.timeout(5e3) });
113
+ const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/${tag}`, { signal: AbortSignal.timeout(5e3) });
102
114
  if (!response.ok) return void 0;
103
115
  const manifest = await response.json();
104
116
  if (typeof manifest.version !== "string") return void 0;
105
- latestCache.set(packageName, {
117
+ latestCache.set(key, {
106
118
  version: manifest.version,
107
119
  at: Date.now()
108
120
  });
@@ -111,6 +123,10 @@ async function npmLatestVersion(packageName) {
111
123
  return;
112
124
  }
113
125
  }
126
+ async function npmLatestVersion(packageName) {
127
+ if (isOfficialRuntimePackage(packageName)) return await npmTaggedVersion(packageName, "next") ?? await npmTaggedVersion(packageName, "latest");
128
+ return npmTaggedVersion(packageName, "latest");
129
+ }
114
130
  function escapeRegExp(value) {
115
131
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
116
132
  }
@@ -156,17 +172,29 @@ function versionParts(version) {
156
172
  Number(match[3])
157
173
  ];
158
174
  }
175
+ function prereleaseRank(version) {
176
+ const match = /-rc\.(\d+)/i.exec(version);
177
+ return match === null ? Number.POSITIVE_INFINITY : Number(match[1]);
178
+ }
159
179
  function newerVersion(installed, latest) {
160
180
  const current = versionParts(installed);
161
181
  const candidate = versionParts(latest);
162
182
  if (current === void 0 || candidate === void 0) return false;
163
- return candidate[0] > current[0] || candidate[0] === current[0] && candidate[1] > current[1] || candidate[0] === current[0] && candidate[1] === current[1] && candidate[2] > current[2];
183
+ if (candidate[0] !== current[0]) return candidate[0] > current[0];
184
+ if (candidate[1] !== current[1]) return candidate[1] > current[1];
185
+ if (candidate[2] !== current[2]) return candidate[2] > current[2];
186
+ return prereleaseRank(latest) > prereleaseRank(installed);
164
187
  }
165
188
  /** 返回 Web profile 中固定管理插件的实际安装版本与 npm latest 状态。 */
166
189
  async function dependencyStatuses() {
190
+ const declaredNames = await declaredPluginNames();
167
191
  return Promise.all(MANAGED_DEPENDENCIES.map(async (dependency) => {
168
192
  const version = await installedPackageVersion(dependency.packageName);
169
- if (!isManagedPackageInstalled({ installedVersion: version })) return {
193
+ const declared = isOfficialRuntimePackage(dependency.packageName) || declaredNames.includes(dependency.packageName);
194
+ if (version === void 0 || !isManagedPackageInstalled({
195
+ installedVersion: version,
196
+ declared
197
+ })) return {
170
198
  ...dependency,
171
199
  installed: false,
172
200
  updateAvailable: false
@@ -190,12 +218,55 @@ function resolveDshCliEntry(entry = process.argv[1], cwd = process.cwd()) {
190
218
  if (entry.startsWith("file:")) return fileURLToPath(entry);
191
219
  return resolve(cwd, entry);
192
220
  }
221
+ function requestDesktopHotUpdate(send = process.send) {
222
+ if (typeof send !== "function") return false;
223
+ send(APPLY_PLUGIN_UPDATES_IPC);
224
+ return true;
225
+ }
226
+ function isRestartableInstallError(error) {
227
+ const message = error instanceof Error ? error.message : String(error);
228
+ return /完全退出桌面端|正在运行的插件|pnpm 仓库不一致/.test(message);
229
+ }
230
+ async function recordPendingUpdate(packageName, version) {
231
+ const pendingPath = resolve(profileDirectory(), PROFILE_PENDING_UPDATES_FILE);
232
+ let packages = [];
233
+ try {
234
+ const parsed = JSON.parse(await readFile(pendingPath, "utf8"));
235
+ if (Array.isArray(parsed.packages)) packages = parsed.packages.flatMap((item) => {
236
+ if (item === null || typeof item !== "object") return [];
237
+ const record = item;
238
+ if (typeof record.packageName !== "string" || typeof record.version !== "string") return [];
239
+ return [{
240
+ packageName: record.packageName,
241
+ version: record.version
242
+ }];
243
+ });
244
+ } catch (error) {
245
+ if (error.code !== "ENOENT") throw error;
246
+ }
247
+ packages = packages.filter((item) => item.packageName !== packageName);
248
+ packages.push({
249
+ packageName,
250
+ version
251
+ });
252
+ await writeFile(pendingPath, `${JSON.stringify({ packages }, void 0, 2)}\n`, "utf8");
253
+ }
254
+ async function recordDeclaredVersion(packageName, version) {
255
+ const manifestPath = resolve(profileDirectory(), "package.json");
256
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
257
+ manifest.dependencies = {
258
+ ...manifest.dependencies,
259
+ [packageName]: version
260
+ };
261
+ await writeFile(manifestPath, `${JSON.stringify(manifest, void 0, 2)}\n`, "utf8");
262
+ }
193
263
  function pluginCommandError(stderr) {
194
264
  const detail = stderr.replace(/\s+/g, " ").trim();
195
265
  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 或发布时间保护后重试。");
266
+ if (/EPERM|EBUSY|EACCES|unable to unlink|ERR_PNPM_LOCKED|Lock/i.test(detail)) return /* @__PURE__ */ new Error("无法覆盖正在运行的插件文件。请先完全退出桌面端,再重新打开后更新。");
267
+ if (/UNEXPECTED_STORE|Unexpected store location/i.test(detail)) return /* @__PURE__ */ new Error("插件目录和 pnpm 仓库不一致。请完全退出桌面端后再更新。");
268
+ if (/pnpm not found/i.test(detail)) return /* @__PURE__ */ new Error("当前环境找不到 pnpm。请从桌面端启动后再更新。");
269
+ return /* @__PURE__ */ new Error("无法在应用运行时更新插件。请先完全退出桌面端,再重新打开后更新。");
199
270
  }
200
271
  /**
201
272
  * 复用启动当前服务的 DSH CLI:它会通过 pnpm 从 npm 安装或更新,并自动维护
@@ -220,19 +291,21 @@ function runDshPlugin(args) {
220
291
  windowsHide: true,
221
292
  stdio: [
222
293
  "ignore",
223
- "ignore",
294
+ "pipe",
224
295
  "pipe"
225
296
  ]
226
297
  });
227
- let stderr = "";
228
- child.stderr?.on("data", (chunk) => {
229
- stderr += String(chunk);
230
- });
298
+ let output = "";
299
+ const collect = (chunk) => {
300
+ output += String(chunk);
301
+ };
302
+ child.stdout?.on("data", collect);
303
+ child.stderr?.on("data", collect);
231
304
  child.once("error", () => {
232
305
  reject(/* @__PURE__ */ new Error("无法启动 DSH 插件安装命令。请确认 Node.js 与 pnpm 可用后重试。"));
233
306
  });
234
307
  child.once("exit", (code) => {
235
- code === 0 ? resolvePromise() : reject(pluginCommandError(stderr));
308
+ code === 0 ? resolvePromise() : reject(pluginCommandError(output));
236
309
  });
237
310
  });
238
311
  }
@@ -260,13 +333,20 @@ async function installDependencyLocked(id) {
260
333
  const remove = pluginsToRemoveBeforeInstall(declared, target);
261
334
  if (remove.length > 0) await runDshPlugin(["remove", ...remove]);
262
335
  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 后再更新。`);
336
+ if (!isOfficialRuntimePackage(target)) await recordDeclaredVersion(target, targetVersion);
337
+ await recordPendingUpdate(target, targetVersion);
338
+ if (requestDesktopHotUpdate()) return dependencyStatuses();
339
+ try {
340
+ await runDshPlugin([
341
+ "add",
342
+ `${target}@${targetVersion}`,
343
+ "--registry=https://registry.npmjs.org/"
344
+ ]);
345
+ } catch (error) {
346
+ if (isRestartableInstallError(error)) return dependencyStatuses();
347
+ throw error;
348
+ }
349
+ if (await installedPackageVersion(target) !== targetVersion) return dependencyStatuses();
270
350
  return dependencyStatuses();
271
351
  }
272
352
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-codex-ui",
3
- "version": "0.2.59",
3
+ "version": "0.2.61",
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
+ }