dsh-sessions-manager 3.4.2 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  > DSH session manager: archive, move, restore, and inspect sessions from **Settings → 会话管理**; mark unread, move, and delete sessions directly from the **main sidebar**. Deleted sessions go to the recycle bin first and can be restored or permanently purged.
14
14
 
15
- A persistent DSH plugin (host + browser halves). Starting with v3.0.0, it covers both the **settings panel** and the **main sidebar**, so frequent session actions don't require opening Settings.
15
+ A persistent DSH plugin (host + browser halves) covering both the **settings panel** and the **main sidebar**, so frequent session actions don't require opening Settings. It understands both legacy header lists/`readFrom` and the new snapshot/`SessionHandle` API, enabling actions according to the current Runtime's verified capabilities. Unverified purge or migration paths are disabled in both UI and Host to prevent false success.
16
16
 
17
17
  ## Features
18
18
 
@@ -43,7 +43,7 @@ A small dot is rendered to the left of each session row. Its color is driven by
43
43
  | Color | State | Description |
44
44
  |---|---|---|
45
45
  | 🔵 Blue | Manually marked unread | Toggle via the ⋯ menu or by clicking the dot; auto-cleared when the session is opened |
46
- | 🟡 Yellow | Working | The session is currently running (`running`) |
46
+ | 🟡 Yellow | Working | The session is currently running (`ongoing`, with legacy `running` compatibility) |
47
47
  | 🟠 Amber | Awaiting feedback | The session has a follow-up question and is waiting for your input or confirmation (`warning`) |
48
48
  | 🟢 Green | Completed but unread | The session is done (`done`) but you haven't reopened it yet; disappears after you view it |
49
49
  | 🔴 Red | Error / needs attention | The session hit an error (`error`) |
@@ -149,6 +149,7 @@ lib/client.js pre-built client (ModuleLoader CJS handshake)
149
149
  | POST | `/archived-sessions/storage` | Storage-usage aggregation (per-workspace ranking + largest sessions) `{ topN }` |
150
150
  | POST | `/archived-sessions/auto-archive/settings` | Read or update the auto-archive policy `{ inactiveDays, skipStarred }`; reading lazily triggers the daily check |
151
151
  | POST | `/archived-sessions/auto-archive/run` | Run a one-off auto-archive check immediately (ignores the daily throttle) |
152
+ | POST | `/archived-sessions/capabilities` | Report the persistence generation and per-action read/archive/trash/purge/cross-workspace-move capabilities |
152
153
 
153
154
  > Deleting a session moves it to the recycle bin by default; only "permanently delete" inside the recycle bin physically removes the log. Permanently deleted sessions are hidden forever on the client side so they don't reappear in the sidebar or "ungrouped" due to DSH runtime caching.
154
155
 
@@ -157,6 +158,9 @@ lib/client.js pre-built client (ModuleLoader CJS handshake)
157
158
  - **Supported platforms**: cross-platform (macOS / Windows / Linux) — wherever DSH runs the plugin runs; the host half is Node (about `^22.19` or `>=24`) and the client is React, with no OS-specific APIs.
158
159
  - Works in both DSH Desktop and DSH web (same host + client halves).
159
160
  - Peer dependencies are listed in `package.json`; `react` and `@deepseek-ai/*` are provided by the DSH runtime.
161
+ - `0.1.2-rc.1`: the existing read, archive, recycle-bin, permanent-purge, and cross-workspace move paths remain available.
162
+ - `0.1.3-alpha.1`: snapshot lists and read-only `SessionHandle` flows are supported. Permanent purge and cross-workspace move are disabled when no verified safe path is available; other management functions continue to work. The panel shows the effective capabilities.
163
+ - Unverified future runtimes expose only capabilities the plugin can safely identify; method presence alone is not presented as behavioral compatibility.
160
164
 
161
165
  ## License
162
166
 
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  > DSH 会话管理器:在**设置 → 会话管理**里统一归档、移动、恢复、查看详情;在**主页侧边栏**直接标记未读、移动、删除会话。删除先进入回收站,可恢复或彻底清理。
14
14
 
15
- 一个 DSH 持久化插件(host + browser 双半)。v3.0.0 起同时覆盖「设置面板」与「主页侧边栏」两个入口,无需打开设置即可完成高频会话操作。
15
+ 一个 DSH 持久化插件(host + browser 双半),同时覆盖「设置面板」与「主页侧边栏」两个入口,无需打开设置即可完成高频会话操作。插件同时识别旧式 header 列表/`readFrom` 与新版 snapshot/`SessionHandle`,并按当前 Runtime 的实际能力启用安全操作;尚未验证的删除或迁移路径会在界面与 Host 端同步禁用,避免假成功。
16
16
 
17
17
  ## 功能
18
18
 
@@ -43,7 +43,7 @@
43
43
  | 颜色 | 状态 | 说明 |
44
44
  |---|---|---|
45
45
  | 🔵 蓝 | 手动标记未读 | 通过 ⋯ 菜单或点击圆点手动标记;进入会话后自动清除 |
46
- | 🟡 黄 | 工作中 | 会话正在运行(`running`) |
46
+ | 🟡 黄 | 工作中 | 会话正在运行(新版 `ongoing`,兼容旧版 `running`) |
47
47
  | 🟠 琥珀 | 等待反馈 | 会话有追问,需要用户输入或确认(`warning`) |
48
48
  | 🟢 绿 | 完成后未读 | 会话已完成(`done`)但你还没重新打开看过;看过一次后不再显示 |
49
49
  | 🔴 红 | 出错 / 需关注 | 会话遇到错误(`error`) |
@@ -149,6 +149,7 @@ lib/client.js 预构建 client(ModuleLoader CJS handshake)
149
149
  | POST | `/archived-sessions/storage` | 存储占用聚合(按工作区排行 + 最大的会话)`{ topN }` |
150
150
  | POST | `/archived-sessions/auto-archive/settings` | 读取或更新自动归档策略 `{ inactiveDays, skipStarred }`;读取时惰性触发每日检查 |
151
151
  | POST | `/archived-sessions/auto-archive/run` | 立即执行一次自动归档检查(忽略每日节流) |
152
+ | POST | `/archived-sessions/capabilities` | 返回当前持久化代际及读取、归档、回收站、永久删除、跨工作区移动能力 |
152
153
 
153
154
  > 删除会话默认进入回收站,只有回收站内的「彻底删除」才会物理移除日志。被彻底删除的会话由前端永久隐藏,避免 DSH 运行时缓存使其重新出现在侧栏或「未分组」中。
154
155
 
@@ -157,6 +158,9 @@ lib/client.js 预构建 client(ModuleLoader CJS handshake)
157
158
  - **适配系统**:跨平台(macOS / Windows / Linux)——只要 DSH 能在该系统运行即可;本插件 host 基于 Node(约 `^22.19` 或 `>=24`)、浏览器端为 React,不依赖特定操作系统 API。
158
159
  - DSH Desktop / web 均可(同一套 host + client)。
159
160
  - peerDependencies 见 `package.json`;`react`、`@deepseek-ai/*` 由 DSH 运行时提供。
161
+ - `0.1.2-rc.1`:现有读取、归档、回收站、永久删除和跨工作区移动能力保持可用。
162
+ - `0.1.3-alpha.1`:支持 snapshot 列表和 `SessionHandle` 只读流程;永久删除与跨工作区移动在缺少已验证安全路径时自动禁用,其余管理能力继续工作。能力以面板实际提示为准。
163
+ - 未经验证的未来 Runtime 默认只开放能够识别的安全能力;插件不会用方法存在与否冒充行为兼容。
160
164
 
161
165
  ## License
162
166
 
package/lib/client.js CHANGED
@@ -40,6 +40,7 @@ var import_react = __toESM(require("react"), 1);
40
40
  function dotStateFor({ manualUnread = false, dataState = "", isActive = false } = {}) {
41
41
  if (manualUnread) return "manual";
42
42
  switch (dataState) {
43
+ case "ongoing":
43
44
  case "running":
44
45
  return "running";
45
46
  case "warning":
@@ -52,6 +53,10 @@ function dotStateFor({ manualUnread = false, dataState = "", isActive = false }
52
53
  return null;
53
54
  }
54
55
  }
56
+ function authoritativeTitleForFirstPaint({ firstPaint = false, rendered = "", authoritative = "" } = {}) {
57
+ if (!firstPaint || !authoritative || rendered === authoritative) return null;
58
+ return authoritative;
59
+ }
55
60
  function canDropOnWorkspace(item, target) {
56
61
  if (!item || !target) return false;
57
62
  if (item.workspacePath && target.path === item.workspacePath) return false;
@@ -278,6 +283,7 @@ function installSettingsNavIcons(ctx) {
278
283
  function SessionPanel({ workspacesSvc }) {
279
284
  const [sessions, setSessions] = (0, import_react.useState)(null);
280
285
  const [workspaces, setWorkspaces] = (0, import_react.useState)([]);
286
+ const [capabilities, setCapabilities] = (0, import_react.useState)(null);
281
287
  const initialPrefs = (0, import_react.useRef)(loadPanelPrefs()).current;
282
288
  const [filter, setFilter] = (0, import_react.useState)(() => ["all", "active", "archived", "starred", "trash"].includes(initialPrefs.filter) ? initialPrefs.filter : "all");
283
289
  const [query, setQuery] = (0, import_react.useState)("");
@@ -326,12 +332,14 @@ function SessionPanel({ workspacesSvc }) {
326
332
  setError(null);
327
333
  Promise.all([
328
334
  postJSON("/archived-sessions/sessions", {}),
329
- postJSON("/archived-sessions/workspaces", {})
330
- ]).then(([s, works]) => {
335
+ postJSON("/archived-sessions/workspaces", {}),
336
+ postJSON("/archived-sessions/capabilities", {})
337
+ ]).then(([s, works, caps]) => {
331
338
  const purged = dsmLoadPurged();
332
339
  const visible = (s.items || []).filter((x) => !purged.has(String(x.sessionId)));
333
340
  setSessions(visible);
334
341
  setWorkspaces(works.items || []);
342
+ setCapabilities(caps || null);
335
343
  setSelected({});
336
344
  setDelTarget(null);
337
345
  setConfirmBatch(false);
@@ -428,6 +436,9 @@ function SessionPanel({ workspacesSvc }) {
428
436
  const w = workspaces.find((x) => x.workspaceId === id);
429
437
  return w ? w.path : "";
430
438
  };
439
+ const actionCapability = (name) => capabilities && capabilities.actions && capabilities.actions[name] || { available: false, reason: "\u6B63\u5728\u68C0\u67E5\u5F53\u524D DSH \u7684\u517C\u5BB9\u80FD\u529B\u2026" };
440
+ const canPurge = actionCapability("purge");
441
+ const canMove = actionCapability("move");
431
442
  const archivedList = sessions ? sessions.filter((x) => x.archived) : [];
432
443
  const activeList = sessions ? sessions.filter((x) => !x.archived) : [];
433
444
  const starredList = starredOf(sessions);
@@ -511,7 +522,9 @@ function SessionPanel({ workspacesSvc }) {
511
522
  postJSON("/archived-sessions/trash/verify", {}).then((r) => {
512
523
  setTrashBusy(null);
513
524
  setTrashCheck(r);
514
- showToast(r.missing ? `\u53D1\u73B0 ${r.missing} \u6761\u65E5\u5FD7\u7F3A\u5931` : "\u56DE\u6536\u7AD9\u6821\u9A8C\u901A\u8FC7");
525
+ if (r.missing) showToast(`\u53D1\u73B0 ${r.missing} \u6761\u65E5\u5FD7\u7F3A\u5931`);
526
+ else if (r.unverified) showToast(`${r.unverified} \u6761\u65E5\u5FD7\u4F4D\u7F6E\u7531\u5F53\u524D Runtime \u7BA1\u7406\uFF0C\u65E0\u6CD5\u76F4\u63A5\u6838\u9A8C`);
527
+ else showToast("\u56DE\u6536\u7AD9\u6821\u9A8C\u901A\u8FC7");
515
528
  }).catch((e) => {
516
529
  setTrashBusy(null);
517
530
  setError(String(e && e.message || e));
@@ -612,6 +625,10 @@ function SessionPanel({ workspacesSvc }) {
612
625
  });
613
626
  };
614
627
  const doMove = (it) => {
628
+ if (!canMove.available) {
629
+ setError(canMove.reason || "\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u8DE8\u5DE5\u4F5C\u533A\u79FB\u52A8");
630
+ return;
631
+ }
615
632
  const targetPath = moveMode === "new" ? newPath.trim() : wsPath(targetWs);
616
633
  if (!targetPath) {
617
634
  setError("\u8BF7\u9009\u62E9\u5DF2\u6709\u5DE5\u4F5C\u533A\u6216\u8F93\u5165\u65B0\u7684\u76EE\u6807\u76EE\u5F55\u8DEF\u5F84");
@@ -653,6 +670,10 @@ function SessionPanel({ workspacesSvc }) {
653
670
  });
654
671
  };
655
672
  const openMoveFor = (it) => {
673
+ if (!canMove.available) {
674
+ setError(canMove.reason || "\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u8DE8\u5DE5\u4F5C\u533A\u79FB\u52A8");
675
+ return;
676
+ }
656
677
  if (openMove === it.sessionId) {
657
678
  setOpenMove(null);
658
679
  return;
@@ -738,17 +759,22 @@ function SessionPanel({ workspacesSvc }) {
738
759
  children: "\u22EF"
739
760
  }
740
761
  ),
741
- openMenu === it.sessionId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "more-menu", role: "menu", children: items.map(([id, label]) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
742
- "button",
743
- {
744
- type: "button",
745
- role: "menuitem",
746
- className: "more-item" + (id === "delete" ? " more-item-danger" : ""),
747
- onClick: () => runMenu(id, it),
748
- children: label
749
- },
750
- id
751
- )) })
762
+ openMenu === it.sessionId && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "more-menu", role: "menu", children: items.map(([id, label]) => {
763
+ const blocked = id === "move" && !canMove.available;
764
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
765
+ "button",
766
+ {
767
+ type: "button",
768
+ role: "menuitem",
769
+ className: "more-item" + (id === "delete" ? " more-item-danger" : ""),
770
+ disabled: blocked,
771
+ title: blocked ? canMove.reason : void 0,
772
+ onClick: () => runMenu(id, it),
773
+ children: label
774
+ },
775
+ id
776
+ );
777
+ }) })
752
778
  ] });
753
779
  };
754
780
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "archv", role: "region", "aria-label": "\u4F1A\u8BDD\u7BA1\u7406", children: [
@@ -1191,22 +1217,25 @@ function SessionPanel({ workspacesSvc }) {
1191
1217
  ] }),
1192
1218
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "sess-field", children: [
1193
1219
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { children: "\u6C38\u4E45\u6E05\u7406" }),
1194
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn archv-del", disabled: trashBusy !== null || !trash.length, onClick: () => setPurgeTarget("__all"), children: "\u6E05\u7A7A\u56DE\u6536\u7AD9" })
1220
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn archv-del", disabled: trashBusy !== null || !trash.length || !canPurge.available, title: !canPurge.available ? canPurge.reason : void 0, onClick: () => setPurgeTarget("__all"), children: "\u6E05\u7A7A\u56DE\u6536\u7AD9" })
1195
1221
  ] })
1196
1222
  ] }),
1223
+ !canPurge.available && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "archv-empty", role: "status", children: canPurge.reason }),
1197
1224
  trashCheck && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: trashCheck.missing ? "archv-err" : "archv-empty", role: "status", children: [
1198
1225
  "\u6821\u9A8C\u5B8C\u6210\uFF1A",
1199
1226
  trashCheck.healthy,
1200
1227
  " \u6761\u6B63\u5E38\uFF0C",
1201
1228
  trashCheck.missing,
1202
- " \u6761\u65E5\u5FD7\u7F3A\u5931\u3002"
1229
+ " \u6761\u65E5\u5FD7\u7F3A\u5931\uFF0C",
1230
+ trashCheck.unverified || 0,
1231
+ " \u6761\u65E0\u6CD5\u76F4\u63A5\u6838\u9A8C\u3002"
1203
1232
  ] }),
1204
1233
  trash.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsm-trash-empty", children: "\u56DE\u6536\u7AD9\u4E3A\u7A7A\u3002\u5220\u9664\u7684\u4F1A\u8BDD\u4F1A\u5148\u8FDB\u5165\u8FD9\u91CC\uFF0C\u53EF\u6062\u590D\u6216\u5F7B\u5E95\u5220\u9664\u3002" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsm-trash-list", children: trash.map((t) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsm-trash-row", children: [
1205
1234
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsm-trash-name", title: t.sessionId, children: t.title || t.sessionId }),
1206
1235
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsm-trash-date", children: fmtDate(t.deletedAt) }),
1207
1236
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dsm-trash-actions", children: [
1208
1237
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn", disabled: trashBusy !== null, onClick: () => restoreTrash(t.sessionId), children: "\u6062\u590D" }),
1209
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn archv-del", disabled: trashBusy !== null, onClick: () => setPurgeTarget(t.sessionId), children: "\u5F7B\u5E95\u5220\u9664" })
1238
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn archv-del", disabled: trashBusy !== null || !canPurge.available, title: !canPurge.available ? canPurge.reason : void 0, onClick: () => setPurgeTarget(t.sessionId), children: "\u5F7B\u5E95\u5220\u9664" })
1210
1239
  ] })
1211
1240
  ] }, t.sessionId)) })
1212
1241
  ] }),
@@ -1258,7 +1287,7 @@ var SIDEBAR_AUG_CSS = `
1258
1287
  .dsm-sub-cur{font-size:11px;color:var(--dsw-alias-state-business-primary);flex:none;margin-left:8px}
1259
1288
  .dsm-dot{position:absolute;left:6px;top:50%;transform:translateY(-50%);width:8px;height:8px;border-radius:50%;box-sizing:border-box;cursor:pointer;pointer-events:auto;z-index:1}
1260
1289
  .dsm-dot-unread-manual{background:var(--dsw-alias-state-business-primary)}
1261
- .dsm-dot-waiting{background:var(--dsw-alias-state-warning-primary)}
1290
+ .dsm-dot-waiting{background:var(--dsw-alias-state-warn-primary,#F59E0B)}
1262
1291
  .dsm-dot-unread{background:var(--dsw-alias-state-success-primary)}
1263
1292
  .dsm-drag-source{opacity:.48}
1264
1293
  .dsm-drop-target{position:relative;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)!important;outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px;border-radius:8px}
@@ -1319,6 +1348,17 @@ var dsmTrashIds = null;
1319
1348
  var dsmServerPurgedIds = /* @__PURE__ */ new Set();
1320
1349
  var dsmAuthoritativeTitles = /* @__PURE__ */ new Map();
1321
1350
  var dsmTrashTick = 0;
1351
+ var dsmCapabilities = null;
1352
+ async function dsmLoadCapabilities() {
1353
+ try {
1354
+ dsmCapabilities = await postJSON("/archived-sessions/capabilities", {});
1355
+ } catch (e) {
1356
+ }
1357
+ return dsmCapabilities;
1358
+ }
1359
+ function dsmActionCapability(name) {
1360
+ return dsmCapabilities && dsmCapabilities.actions && dsmCapabilities.actions[name];
1361
+ }
1322
1362
  async function dsmLoadTrashIds() {
1323
1363
  try {
1324
1364
  const r = await postJSON("/archived-sessions/sidebar-state", {});
@@ -1549,6 +1589,11 @@ function installSidebarSessionMenuAug() {
1549
1589
  const move = mk("\u79FB\u52A8\u4F1A\u8BDD", ICON_MOVE, false);
1550
1590
  const del = mk("\u5220\u9664\u4F1A\u8BDD", ICON_DEL, true);
1551
1591
  const mark = mk(dsmLoadManual().has(info.id) ? "\u6807\u8BB0\u5DF2\u8BFB" : "\u6807\u8BB0\u672A\u8BFB", ICON_UNREAD, false);
1592
+ const moveCapability = dsmActionCapability("move");
1593
+ if (!moveCapability || !moveCapability.available) {
1594
+ move.btn.disabled = true;
1595
+ move.btn.title = moveCapability && moveCapability.reason || "\u6B63\u5728\u68C0\u67E5\u5F53\u524D\u7248\u672C\u7684\u79FB\u52A8\u80FD\u529B";
1596
+ }
1552
1597
  if (mark.btn.firstChild) mark.btn.firstChild.style.color = "var(--dsw-alias-state-business-primary)";
1553
1598
  const chev = document.createElement("span");
1554
1599
  chev.style.cssText = "margin-left:auto;flex:none;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:1";
@@ -1635,7 +1680,7 @@ function installSidebarStatusDots() {
1635
1680
  // 蓝 手动标记未读
1636
1681
  running: "#EAB308",
1637
1682
  // 黄 工作中 (DSH running)
1638
- feedback: "var(--dsw-alias-state-warning-primary)",
1683
+ feedback: "var(--dsw-alias-state-warn-primary, #F59E0B)",
1639
1684
  // 琥珀 需用户反馈 (DSH warning)
1640
1685
  done: "var(--dsw-alias-state-success-primary)",
1641
1686
  // 绿 完成后未读 (DSH done)
@@ -1643,6 +1688,7 @@ function installSidebarStatusDots() {
1643
1688
  // 红 出错/需关注 (DSH error)
1644
1689
  };
1645
1690
  const manualUnread = dsmLoadManual();
1691
+ const titleInitializedRows = /* @__PURE__ */ new WeakSet();
1646
1692
  const paint = () => {
1647
1693
  const activeId = activeRowId();
1648
1694
  if (activeId !== curActive && activeId && manualUnread.has(activeId)) {
@@ -1663,12 +1709,20 @@ function installSidebarStatusDots() {
1663
1709
  }
1664
1710
  if (row.style.display === "none") row.style.display = "";
1665
1711
  const authoritativeTitle = dsmAuthoritativeTitles.get(id);
1666
- if (authoritativeTitle) {
1712
+ if (!titleInitializedRows.has(row)) {
1667
1713
  const node = rowNode(row) || {};
1668
1714
  const expected = new Set([node.title, node.displayTitle, node.name].filter((value) => typeof value === "string"));
1669
1715
  const spans = [...row.children].filter((el) => el.tagName === "SPAN" && !el.querySelector("[data-state]") && (el.textContent || "").trim());
1670
1716
  const titleEl = spans.find((el) => expected.has((el.textContent || "").trim())) || spans[0];
1671
- if (titleEl && titleEl.textContent !== authoritativeTitle) titleEl.textContent = authoritativeTitle;
1717
+ if (titleEl) {
1718
+ const correction = authoritativeTitleForFirstPaint({
1719
+ firstPaint: true,
1720
+ rendered: titleEl.textContent || "",
1721
+ authoritative: authoritativeTitle || ""
1722
+ });
1723
+ if (correction) titleEl.textContent = correction;
1724
+ titleInitializedRows.add(row);
1725
+ }
1672
1726
  }
1673
1727
  const sd = row.querySelector("[data-state]");
1674
1728
  if (sd) sd.style.display = "none";
@@ -1785,6 +1839,8 @@ function installSidebarWorkspaceDrag() {
1785
1839
  maybeRefresh();
1786
1840
  }, true);
1787
1841
  document.addEventListener("dragstart", (event) => {
1842
+ const moveCapability = dsmActionCapability("move");
1843
+ if (!moveCapability || !moveCapability.available) return;
1788
1844
  const row = eventRow(event);
1789
1845
  const item = row && sessionForRow(row);
1790
1846
  if (!item || moving) return;
@@ -1864,6 +1920,7 @@ function installSidebarWorkspaceDrag() {
1864
1920
  }
1865
1921
  }
1866
1922
  function apply(ctx) {
1923
+ dsmLoadCapabilities();
1867
1924
  installSettingsNavIcons(ctx);
1868
1925
  installSidebarSessionMenuAug();
1869
1926
  installSidebarStatusDots();