dsh-sessions-manager 3.5.0 → 3.5.2

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,9 +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.
16
-
17
- v3.5.0 adds a two-generation DSH persistence adapter. It understands both legacy header lists/`readFrom` and the new snapshot/`SessionHandle` API, and reports action-level capabilities to the UI. When a runtime has no verified safe purge or migration path, the corresponding controls explain why they are disabled and the Host rejects direct calls, preventing false success messages.
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.
18
16
 
19
17
  ## Features
20
18
 
@@ -45,7 +43,7 @@ A small dot is rendered to the left of each session row. Its color is driven by
45
43
  | Color | State | Description |
46
44
  |---|---|---|
47
45
  | 🔵 Blue | Manually marked unread | Toggle via the ⋯ menu or by clicking the dot; auto-cleared when the session is opened |
48
- | 🟡 Yellow | Working | The session is currently running (`running`) |
46
+ | 🟡 Yellow | Working | The session is currently running (`ongoing`, with legacy `running` compatibility) |
49
47
  | 🟠 Amber | Awaiting feedback | The session has a follow-up question and is waiting for your input or confirmation (`warning`) |
50
48
  | 🟢 Green | Completed but unread | The session is done (`done`) but you haven't reopened it yet; disappears after you view it |
51
49
  | 🔴 Red | Error / needs attention | The session hit an error (`error`) |
@@ -164,6 +162,16 @@ lib/client.js pre-built client (ModuleLoader CJS handshake)
164
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.
165
163
  - Unverified future runtimes expose only capabilities the plugin can safely identify; method presence alone is not presented as behavioral compatibility.
166
164
 
165
+ ### Behavior differences and degradations under `0.1.3-alpha.1`
166
+
167
+ - **Recycle-bin restore is verified**: before restoring, the plugin confirms the underlying stored session still exists (live / `stat` / listing, in that order). It returns accurate errors when the session is gone (`DSM_SESSION_MISSING`), was permanently purged (`DSM_SESSION_PURGED`), or the index still lists it but the log file has vanished (`DSM_SESSION_LOG_MISSING`); when the log location cannot be verified the entry is honestly reported as `unverified` instead of silently passing. Restoring a session whose workspace was deleted succeeds and notes that it now lives under "未分组" (ungrouped).
168
+ - **Permanent purge / empty recycle bin / automatic physical cleanup**: the official public contract offers no delete API, so the plugin keeps the legacy-era semi-official approach — a **three-layer guarded path derivation** starting from the backend instance's storage-root field (root → session directory structure → session-id ownership check), followed by a whole-directory removal verified with the official `stat`. Sessions with an active writer are refused (409). When the derivation fails (e.g. the storage root is unavailable), the actions degrade to disabled with an explicit reason instead of deleting blindly.
169
+ - **Cross-workspace move**: the primary path replays events through the official `create` + `append` (writer-ownership probe refuses active sessions, before/after `revision` checks guard against concurrent writes, backup + rollback guarantees no half-moved state on failure); when the backend holds a ghost record for the same id it falls back to relocating the log with a frame0 cwd rewrite (frame count and content verified byte-for-byte). Paths come from the same guarded derivation; the workspace grouping index is rebuilt automatically after the move — no restart needed.
170
+ - **Purge tombstones never suppress new sessions**: if a session with the same id is recreated later, the tombstone yields automatically and the new session appears normally in lists and the sidebar.
171
+ - **Metadata-cache change tokens**: on the new runtime the plugin prefers the official `stat`/`list` snapshot `revision` (comparable only within one service instance and one session id) to detect changes; legacy runtimes keep the `(mtime, size)` file fingerprint. Revisions are never persisted into the cross-process title index. List metadata comes straight from `snapshot.header` and titles from the batch projection — a missing title does **not** trigger a full-log decode, avoiding amplification of the known historical-session loading regression in alpha.
172
+ - **Auto-archive**: the new runtime exposes no reliable "last activity" timestamp; the sweep skips (`no-activity-data`) when idleness cannot be proven, and never archives on a guess.
173
+ - **Sidebar injection**: DOM / React fiber recognition is centralized in a versioned adapter; when the upstream sidebar shape is no longer recognized, all injections degrade safely and the official sidebar is left untouched. While recognition works, updates are driven incrementally by a MutationObserver with only a low-frequency fallback check.
174
+
167
175
  ## License
168
176
 
169
177
  [MIT](./LICENSE) © TOBYCAI
package/README.md CHANGED
@@ -12,9 +12,7 @@
12
12
 
13
13
  > DSH 会话管理器:在**设置 → 会话管理**里统一归档、移动、恢复、查看详情;在**主页侧边栏**直接标记未读、移动、删除会话。删除先进入回收站,可恢复或彻底清理。
14
14
 
15
- 一个 DSH 持久化插件(host + browser 双半)。v3.0.0 起同时覆盖「设置面板」与「主页侧边栏」两个入口,无需打开设置即可完成高频会话操作。
16
-
17
- v3.5.0 新增 DSH 双代持久化适配:同时识别旧式 header 列表/`readFrom` 与新版 snapshot/`SessionHandle`,并向界面报告每项操作的实际能力。当前 Runtime 没有经过验证的安全删除或迁移路径时,相关按钮会显示禁用原因,Host 也会拒绝调用,避免出现“提示成功但日志没有删除/移动”的假成功。
15
+ 一个 DSH 持久化插件(host + browser 双半),同时覆盖「设置面板」与「主页侧边栏」两个入口,无需打开设置即可完成高频会话操作。插件同时识别旧式 header 列表/`readFrom` 与新版 snapshot/`SessionHandle`,并按当前 Runtime 的实际能力启用安全操作;尚未验证的删除或迁移路径会在界面与 Host 端同步禁用,避免假成功。
18
16
 
19
17
  ## 功能
20
18
 
@@ -45,7 +43,7 @@ v3.5.0 新增 DSH 双代持久化适配:同时识别旧式 header 列表/`read
45
43
  | 颜色 | 状态 | 说明 |
46
44
  |---|---|---|
47
45
  | 🔵 蓝 | 手动标记未读 | 通过 ⋯ 菜单或点击圆点手动标记;进入会话后自动清除 |
48
- | 🟡 黄 | 工作中 | 会话正在运行(`running`) |
46
+ | 🟡 黄 | 工作中 | 会话正在运行(新版 `ongoing`,兼容旧版 `running`) |
49
47
  | 🟠 琥珀 | 等待反馈 | 会话有追问,需要用户输入或确认(`warning`) |
50
48
  | 🟢 绿 | 完成后未读 | 会话已完成(`done`)但你还没重新打开看过;看过一次后不再显示 |
51
49
  | 🔴 红 | 出错 / 需关注 | 会话遇到错误(`error`) |
@@ -164,6 +162,16 @@ lib/client.js 预构建 client(ModuleLoader CJS handshake)
164
162
  - `0.1.3-alpha.1`:支持 snapshot 列表和 `SessionHandle` 只读流程;永久删除与跨工作区移动在缺少已验证安全路径时自动禁用,其余管理能力继续工作。能力以面板实际提示为准。
165
163
  - 未经验证的未来 Runtime 默认只开放能够识别的安全能力;插件不会用方法存在与否冒充行为兼容。
166
164
 
165
+ ### `0.1.3-alpha.1` 下的行为差异与降级说明
166
+
167
+ - **回收站恢复需要校验**:恢复前会确认底层会话仍存在(live / `stat` / 列表三级判定)。底层会话已不存在(`DSM_SESSION_MISSING`)、已被彻底删除(`DSM_SESSION_PURGED`)或索引仍在但日志文件消失(`DSM_SESSION_LOG_MISSING`)时返回准确错误;无法核验日志位置时如实标注 `unverified`,不会假装校验通过。恢复到「工作区已删除」的会话会成功并提示其暂归「未分组」。
168
+ - **彻底删除 / 清空回收站 / 自动物理清理**:官方公共契约未提供删除 API,插件沿用 legacy 时代的半官方路线——从后端实例的存储根目录字段出发做**三层守卫式路径推导**(根目录 → 会话目录结构 → 会话 ID 归属校验),通过后整目录删除并以官方 `stat` 复核;存在活跃写入的会话会被拒绝(409)。推导失败(如无法确认存储根目录)时自动禁用并说明原因,绝不盲删。
169
+ - **跨工作区移动**:主路径为官方 `create` + `append` 事件重放(写所有权探测拒绝活跃会话、revision 前后校验防并发写入、备份回滚保证失败不留半移动状态);后端存在同 ID 幽灵记录时回退到 frame0 cwd 改写搬运(帧数与内容逐位校验)。路径同样来自守卫式推导;移动后自动重建工作区分组索引,无需重启。
170
+ - **彻底删除墓碑不压制新会话**:若同 ID 会话被重新创建,墓碑自动让位,新会话正常出现在列表与侧栏。
171
+ - **元数据缓存的变更令牌**:新 Runtime 优先使用官方 `stat`/`list` 快照的 `revision`(仅同一 service 实例、同一会话内可比较)判断会话是否变化,旧 Runtime 继续使用 `(mtime, size)` 文件指纹;`revision` 绝不写入跨进程持久索引。列表元数据直接取自 `snapshot.header`,标题走批量投影——没有标题时**不会**自动解码整本日志,避免放大 alpha 已知的历史会话加载性能回退。
172
+ - **自动归档**:新 Runtime 不提供可靠的「最后活跃时间」,无法证明会话闲置时检查会跳过(`no-activity-data`),绝不基于猜测归档。
173
+ - **侧栏注入**:DOM / React fiber 识别收敛为可版本化的 adapter;上游侧栏结构不被识别时整体安全停用,不影响官方侧栏本身。识别正常时以 MutationObserver 增量驱动为主,仅保留低频兜底检查。
174
+
167
175
  ## License
168
176
 
169
177
  [MIT](./LICENSE) © TOBYCAI
package/lib/client.js CHANGED
@@ -40,9 +40,12 @@ 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":
47
+ case "waiting":
48
+ case "needs-attention":
46
49
  return "feedback";
47
50
  case "error":
48
51
  return "error";
@@ -52,6 +55,10 @@ function dotStateFor({ manualUnread = false, dataState = "", isActive = false }
52
55
  return null;
53
56
  }
54
57
  }
58
+ function authoritativeTitleForFirstPaint({ firstPaint = false, rendered = "", authoritative = "" } = {}) {
59
+ if (!firstPaint || !authoritative || rendered === authoritative) return null;
60
+ return authoritative;
61
+ }
55
62
  function canDropOnWorkspace(item, target) {
56
63
  if (!item || !target) return false;
57
64
  if (item.workspacePath && target.path === item.workspacePath) return false;
@@ -91,12 +98,12 @@ function loadPanelPrefs() {
91
98
  }
92
99
  }
93
100
  var CSS = `
94
- .archv{--dsm-radius-tag:4px;display:flex;flex-direction:column;gap:4px;max-width:800px;padding:8px 2px 28px}
101
+ .archv{--dsm-radius-tag:4px;--dsm-radius-ctl:9px;--dsm-radius-sheet:10px;--dsm-radius-card:12px;display:flex;flex-direction:column;gap:4px;max-width:800px;padding:8px 2px 28px}
95
102
  .archv-head{display:flex;align-items:center;gap:10px;margin:0 0 2px}
96
103
  .archv-title{font-size:16px;font-weight:650;color:var(--dsw-alias-label-primary);letter-spacing:-0.01em;margin:0}
97
104
  .archv-count{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-subtle);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:1px 8px;flex:none}
98
105
  .archv-sub{font-size:12px;line-height:1.55;color:var(--dsw-alias-label-tertiary);margin:0 0 12px;max-width:64ch}
99
- .archv-err{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 12px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent);border-radius:10px;color:var(--dsw-alias-state-error-primary);font-size:12px;margin-bottom:10px}
106
+ .archv-err{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 12px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent);border-radius:var(--dsm-radius-sheet);color:var(--dsw-alias-state-error-primary);font-size:12px;margin-bottom:10px}
100
107
  .archv-errretry{appearance:none;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 45%,transparent);background:transparent;color:inherit;border-radius:8px;padding:4px 10px;font-size:11px;cursor:pointer;flex:none}
101
108
  .sess-filter{display:flex;align-items:center;gap:6px;margin:0 0 12px}
102
109
  .sess-fbtn{appearance:none;min-height:30px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-fill-subtle);color:var(--dsw-alias-label-secondary);border-radius:999px;font-size:12px;font-weight:500;cursor:pointer}
@@ -105,13 +112,13 @@ var CSS = `
105
112
  .sess-tools{display:grid;grid-template-columns:minmax(180px,1fr) minmax(140px,.7fr) minmax(130px,.55fr);gap:8px;margin:0 0 8px}
106
113
  .sess-field{display:flex;flex-direction:column;gap:5px;min-width:0}
107
114
  .sess-field label{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary)}
108
- .sess-field input,.sess-field select{box-sizing:border-box;width:100%;min-height:36px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-fill-elevated);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px}
115
+ .sess-field input,.sess-field select{box-sizing:border-box;width:100%;min-height:36px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-ctl);background:var(--dsw-alias-fill-elevated);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px}
109
116
  .sess-results{font-size:11px;color:var(--dsw-alias-label-tertiary);margin:0 0 4px}
110
117
  .archv button:focus-visible,.archv input:focus-visible,.archv select:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
111
118
  .sess-batch{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 2px 4px;margin-bottom:4px}
112
119
  .sess-btntext{font-size:12px;color:var(--dsw-alias-label-tertiary);flex:none}
113
120
  .archv-list{display:flex;flex-direction:column;gap:8px}
114
- .archv-card{display:flex;flex-direction:column;align-items:stretch;gap:0;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-fill-elevated);transition:border-color .15s ease,background-color .15s ease}
121
+ .archv-card{display:flex;flex-direction:column;align-items:stretch;gap:0;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-card);background:var(--dsw-alias-fill-elevated);transition:border-color .15s ease,background-color .15s ease}
115
122
  .archv-card:hover{border-color:var(--dsw-alias-border-l4)}
116
123
  .archv-card-exp{border-color:var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1)}
117
124
  .archv-row{display:flex;align-items:center;gap:14px;width:100%;min-width:0}
@@ -132,16 +139,16 @@ var CSS = `
132
139
  .archv-star-on svg{fill:currentColor}
133
140
  .archv-body{flex:1;min-width:0;display:flex;align-items:center;gap:12px}
134
141
  .archv-actions{display:flex;gap:8px;flex:none;flex-wrap:nowrap;justify-content:flex-end}
135
- .archv-btn{appearance:none;min-height:32px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-fill-subtle);color:var(--dsw-alias-label-secondary);border-radius:9px;font-size:12px;font-weight:500;cursor:pointer;white-space:nowrap;display:inline-flex;align-items:center;justify-content:center;gap:6px;text-align:center;transition:background-color .15s ease,border-color .15s ease,color .15s ease}
142
+ .archv-btn{appearance:none;min-height:32px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-fill-subtle);color:var(--dsw-alias-label-secondary);border-radius:var(--dsm-radius-ctl);font-size:12px;font-weight:500;cursor:pointer;white-space:nowrap;display:inline-flex;align-items:center;justify-content:center;gap:6px;text-align:center;transition:background-color .15s ease,border-color .15s ease,color .15s ease}
136
143
  .archv-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
137
144
  .archv-btn:disabled{opacity:.5;cursor:default}
138
145
  .archv-del{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 45%,transparent)}
139
146
  .archv-del:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
140
147
  .archv-go{color:var(--dsw-alias-state-business-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 45%,transparent)}
141
148
  .archv-go:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);color:var(--dsw-alias-state-business-primary)}
142
- .archv-empty{display:flex;align-items:center;gap:10px;padding:20px 14px;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
149
+ .archv-empty{display:flex;align-items:center;gap:10px;padding:20px 14px;border:1px dashed var(--dsw-alias-border-l3);border-radius:var(--dsm-radius-card);color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
143
150
  .archv-skel{display:flex;flex-direction:column;gap:8px}
144
- .archv-skel-card{height:58px;border-radius:12px;background:var(--dsw-alias-fill-subtle);position:relative;overflow:hidden}
151
+ .archv-skel-card{height:58px;border-radius:var(--dsm-radius-card);background:var(--dsw-alias-fill-subtle);position:relative;overflow:hidden}
145
152
  .archv-skel-card::after{content:'';position:absolute;inset:0;transform:translateX(-100%);background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--dsw-alias-fill-elevated) 75%,transparent),transparent);animation:archv-shimmer 1.4s infinite}
146
153
  .archv-status{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:60;background:var(--dsw-alias-fill-elevated);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);padding:9px 16px;border-radius:999px;font-size:12px;box-shadow:0 8px 24px rgb(0 0 0/.18);display:flex;align-items:center;gap:8px;animation:archv-pop .18s ease-out;max-width:min(90vw,420px)}
147
154
  .archv-spin{width:12px;height:12px;border:2px solid color-mix(in srgb,var(--dsw-alias-label-secondary) 35%,transparent);border-top-color:var(--dsw-alias-label-secondary);border-radius:50%;animation:archv-rot .8s linear infinite;flex:none}
@@ -150,24 +157,24 @@ var CSS = `
150
157
  @keyframes archv-pop{from{opacity:0;transform:translateX(-50%) translateY(10px)}}
151
158
  @media (prefers-reduced-motion:reduce){.archv-skel-card::after{animation:none}.archv-card,.archv-btn{transition:none}.archv-status,.archv-spin{animation:none}}
152
159
  @media (max-width:640px){.archv-card{flex-direction:column;align-items:stretch;gap:10px}.archv-actions{justify-content:flex-end}.sess-tools{grid-template-columns:1fr}.sess-fbtn,.archv-btn{min-height:40px}}
153
- .mv-sheet{width:100%;box-sizing:border-box;display:flex;flex-direction:column;gap:12px;margin-top:12px;padding:14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-fill-subtle)}
160
+ .mv-sheet{width:100%;box-sizing:border-box;display:flex;flex-direction:column;gap:12px;margin-top:12px;padding:14px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-sheet);background:var(--dsw-alias-fill-subtle)}
154
161
  .mv-sheet-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
155
162
  .mv-sheet-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}
156
163
  .mv-sheet-close{appearance:none;width:26px;height:26px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-radius:7px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:16px;line-height:1}
157
164
  .mv-sheet-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
158
- .mv-seg{display:flex;gap:2px;padding:2px;background:var(--dsw-alias-fill-elevated);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;width:100%}
165
+ .mv-seg{display:flex;gap:2px;padding:2px;background:var(--dsw-alias-fill-elevated);border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-sheet);width:100%}
159
166
  .mv-segbtn{appearance:none;flex:1;min-height:30px;padding:0 12px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);border-radius:8px;font-size:12px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:6px}
160
167
  .mv-segbtn:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
161
168
  .mv-segbtn-on{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);box-shadow:0 1px 2px rgb(0 0 0/.08)}
162
169
  .mv-field{display:flex;flex-direction:column;gap:6px}
163
170
  .mv-field label.mv-field-label{font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary)}
164
- .mv-field select,.mv-field input[type=text]{box-sizing:border-box;appearance:none;width:100%;min-height:34px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-fill-elevated);color:var(--dsw-alias-label-primary);font-size:12px;font-family:inherit}
171
+ .mv-field select,.mv-field input[type=text]{box-sizing:border-box;appearance:none;width:100%;min-height:34px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-ctl);background:var(--dsw-alias-fill-elevated);color:var(--dsw-alias-label-primary);font-size:12px;font-family:inherit}
165
172
  .mv-field select:focus-visible,.mv-field input[type=text]:focus-visible,.mv-sheet-close:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
166
173
  .mv-browse-row{display:flex;align-items:center;gap:8px}
167
174
  .mv-browse-row input[type=text]{flex:1;min-width:0}
168
175
  .mv-foot{display:flex;justify-content:flex-end;align-items:center;gap:8px;margin-top:2px}
169
176
  @media (max-width:640px){.archv-row{flex-wrap:wrap}.mv-sheet{padding:12px}}
170
- .dtl-sheet{margin-top:12px;padding:14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-fill-subtle)}
177
+ .dtl-sheet{margin-top:12px;padding:14px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-sheet);background:var(--dsw-alias-fill-subtle)}
171
178
  .dtl-sheet-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
172
179
  .dtl-sheet-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}
173
180
  .dtl-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:8px;margin-top:10px}
@@ -188,7 +195,7 @@ var CSS = `
188
195
  .more-wrap{position:relative;flex:none}
189
196
  .more-btn{appearance:none;width:28px;height:28px;border:none;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;line-height:1}
190
197
  .more-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
191
- .more-menu{position:absolute;top:calc(100% + 4px);right:0;z-index:60;min-width:160px;padding:5px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l3);border-radius:10px;box-shadow:0 10px 32px rgb(0 0 0/.24);display:flex;flex-direction:column;gap:1px}
198
+ .more-menu{position:absolute;top:calc(100% + 4px);right:0;z-index:60;min-width:160px;padding:5px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsm-radius-sheet);box-shadow:0 10px 32px rgb(0 0 0/.24);display:flex;flex-direction:column;gap:1px}
192
199
  .more-item{appearance:none;display:flex;align-items:center;gap:8px;width:100%;padding:7px 10px;border:none;background:transparent;color:var(--dsw-alias-label-primary);border-radius:7px;font-size:12.5px;cursor:pointer;white-space:nowrap;text-align:left}
193
200
  .more-item:hover{background:var(--dsw-alias-interactive-bg-hover)}
194
201
  .more-item-danger{color:var(--dsw-alias-state-error-primary)}
@@ -431,9 +438,13 @@ function SessionPanel({ workspacesSvc }) {
431
438
  const w = workspaces.find((x) => x.workspaceId === id);
432
439
  return w ? w.path : "";
433
440
  };
434
- 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" };
435
- const canPurge = actionCapability("purge");
436
- const canMove = actionCapability("move");
441
+ const actionCapability = (name, legacy) => {
442
+ const source = capabilities && capabilities.actions;
443
+ return source && (source[name] || legacy && source[legacy]) || { available: false, reason: "\u6B63\u5728\u68C0\u67E5\u5F53\u524D DSH \u7684\u517C\u5BB9\u80FD\u529B\u2026" };
444
+ };
445
+ const canPurge = actionCapability("physicalPurge", "purge");
446
+ const canMove = actionCapability("relocateSession", "move");
447
+ const canRestoreTrash = actionCapability("restoreIndexedSession", "restoreTrash");
437
448
  const archivedList = sessions ? sessions.filter((x) => x.archived) : [];
438
449
  const activeList = sessions ? sessions.filter((x) => !x.archived) : [];
439
450
  const starredList = starredOf(sessions);
@@ -574,9 +585,9 @@ function SessionPanel({ workspacesSvc }) {
574
585
  const restoreTrash = (sid) => {
575
586
  if (trashBusy) return;
576
587
  setTrashBusy(sid);
577
- postJSON("/archived-sessions/trash/restore", { sessionId: sid }).then(() => {
588
+ postJSON("/archived-sessions/trash/restore", { sessionId: sid }).then((r) => {
578
589
  setTrashBusy(null);
579
- showToast("\u5DF2\u6062\u590D\u4F1A\u8BDD");
590
+ showToast(r && r.workspaceGone ? "\u5DF2\u6062\u590D\u4F1A\u8BDD\uFF08\u539F\u5DE5\u4F5C\u533A\u5DF2\u5220\u9664\uFF0C\u4F1A\u8BDD\u6682\u5F52\u300C\u672A\u5206\u7EC4\u300D\uFF09" : "\u5DF2\u6062\u590D\u4F1A\u8BDD");
580
591
  loadTrash();
581
592
  refresh();
582
593
  }).catch((e) => {
@@ -1212,10 +1223,16 @@ function SessionPanel({ workspacesSvc }) {
1212
1223
  ] }),
1213
1224
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "sess-field", children: [
1214
1225
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { children: "\u6C38\u4E45\u6E05\u7406" }),
1215
- /* @__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" })
1226
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn archv-del", disabled: trashBusy !== null || !trash.length || !canPurge.available, "aria-disabled": !canPurge.available, title: !canPurge.available ? canPurge.reason : "\u7269\u7406\u5220\u9664\u8FD9\u4E9B\u65E5\u5FD7\uFF0C\u91CA\u653E\u78C1\u76D8\u7A7A\u95F4", onClick: () => setPurgeTarget("__all"), children: "\u6E05\u7A7A\u56DE\u6536\u7AD9" })
1216
1227
  ] })
1217
1228
  ] }),
1218
- !canPurge.available && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "archv-empty", role: "status", children: canPurge.reason }),
1229
+ !canPurge.available && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "archv-empty", role: "status", children: [
1230
+ canPurge.reason,
1231
+ "\u3002\u56DE\u6536\u7AD9\u6761\u76EE\u4F1A\u4E00\u76F4\u4FDD\u7559\uFF0C\u53EF\u968F\u65F6\u6062\u590D\uFF1B\u5F53\u524D DSH \u7248\u672C\u4E0B\u79FB\u5165\u56DE\u6536\u7AD9",
1232
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("b", { children: "\u4E0D\u4F1A\u91CA\u653E\u78C1\u76D8\u7A7A\u95F4" }),
1233
+ "\uFF0C\u4F1A\u8BDD\u65E5\u5FD7\u4ECD\u5B8C\u6574\u4FDD\u7559\u5728\u539F\u5DE5\u4F5C\u533A\u76EE\u5F55\u3002"
1234
+ ] }),
1235
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtl-note", children: "\u56DE\u6536\u7AD9\u662F\u8F6F\u5220\u9664\uFF1A\u65E5\u5FD7\u4ECD\u7559\u5728\u539F\u5DE5\u4F5C\u533A\u76EE\u5F55\uFF0C\u300C\u5F7B\u5E95\u5220\u9664\u300D\u624D\u662F\u771F\u6B63\u91CA\u653E\u78C1\u76D8\u7A7A\u95F4\u7684\u4E00\u6B65\uFF08\u5F53\u524D\u7248\u672C\u672A\u652F\u6301\u65F6\u4F1A\u4FDD\u6301\u7981\u7528\uFF09\u3002" }),
1219
1236
  trashCheck && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: trashCheck.missing ? "archv-err" : "archv-empty", role: "status", children: [
1220
1237
  "\u6821\u9A8C\u5B8C\u6210\uFF1A",
1221
1238
  trashCheck.healthy,
@@ -1229,7 +1246,7 @@ function SessionPanel({ workspacesSvc }) {
1229
1246
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsm-trash-name", title: t.sessionId, children: t.title || t.sessionId }),
1230
1247
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsm-trash-date", children: fmtDate(t.deletedAt) }),
1231
1248
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dsm-trash-actions", children: [
1232
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn", disabled: trashBusy !== null, onClick: () => restoreTrash(t.sessionId), children: "\u6062\u590D" }),
1249
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn", disabled: trashBusy !== null || !canRestoreTrash.available, title: !canRestoreTrash.available ? canRestoreTrash.reason : "\u6062\u590D\u5230\u5220\u9664\u524D\u7684\u4F4D\u7F6E\u4E0E\u5F52\u6863\u72B6\u6001", onClick: () => restoreTrash(t.sessionId), children: "\u6062\u590D" }),
1233
1250
  /* @__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" })
1234
1251
  ] })
1235
1252
  ] }, t.sessionId)) })
@@ -1240,7 +1257,7 @@ function SessionPanel({ workspacesSvc }) {
1240
1257
  purgeTarget === "__all" ? "\u5168\u90E8\u56DE\u6536\u7AD9\u4F1A\u8BDD" : "\u8FD9\u4E2A\u4F1A\u8BDD",
1241
1258
  "\uFF1F"
1242
1259
  ] }),
1243
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dlg-text", children: "\u6B64\u64CD\u4F5C\u4F1A\u7269\u7406\u5220\u9664\u65E5\u5FD7\uFF0C\u65E0\u6CD5\u6062\u590D\u3002\u5F52\u6863\u3001\u7B5B\u9009\u548C\u91CD\u65B0\u5B89\u88C5\u63D2\u4EF6\u90FD\u4E0D\u80FD\u627E\u56DE\u8FD9\u4E9B\u6570\u636E\u3002" }),
1260
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dlg-text", children: "\u6B64\u64CD\u4F5C\u4F1A\u7269\u7406\u5220\u9664\u65E5\u5FD7\u5E76\u91CA\u653E\u78C1\u76D8\u7A7A\u95F4\uFF0C\u65E0\u6CD5\u6062\u590D\u3002\u5F52\u6863\u3001\u7B5B\u9009\u548C\u91CD\u65B0\u5B89\u88C5\u63D2\u4EF6\u90FD\u4E0D\u80FD\u627E\u56DE\u8FD9\u4E9B\u6570\u636E\u3002" }),
1244
1261
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dlg-actions", children: [
1245
1262
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn", onClick: () => setPurgeTarget(null), children: "\u53D6\u6D88" }),
1246
1263
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn archv-del", onClick: () => {
@@ -1282,7 +1299,7 @@ var SIDEBAR_AUG_CSS = `
1282
1299
  .dsm-sub-cur{font-size:11px;color:var(--dsw-alias-state-business-primary);flex:none;margin-left:8px}
1283
1300
  .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}
1284
1301
  .dsm-dot-unread-manual{background:var(--dsw-alias-state-business-primary)}
1285
- .dsm-dot-waiting{background:var(--dsw-alias-state-warning-primary)}
1302
+ .dsm-dot-waiting{background:var(--dsw-alias-state-warn-primary,#F59E0B)}
1286
1303
  .dsm-dot-unread{background:var(--dsw-alias-state-success-primary)}
1287
1304
  .dsm-drag-source{opacity:.48}
1288
1305
  .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}
@@ -1351,57 +1368,83 @@ async function dsmLoadCapabilities() {
1351
1368
  }
1352
1369
  return dsmCapabilities;
1353
1370
  }
1354
- function dsmActionCapability(name) {
1355
- return dsmCapabilities && dsmCapabilities.actions && dsmCapabilities.actions[name];
1371
+ function dsmActionCapability(name, legacy) {
1372
+ const source = dsmCapabilities && dsmCapabilities.actions;
1373
+ return source ? source[name] || legacy && source[legacy] || null : null;
1356
1374
  }
1357
1375
  async function dsmLoadTrashIds() {
1358
1376
  try {
1359
1377
  const r = await postJSON("/archived-sessions/sidebar-state", {});
1360
1378
  dsmTrashIds = new Set((r && r.trashedSessionIds || []).map(String));
1361
1379
  dsmServerPurgedIds = new Set((r && r.purgedSessionIds || []).map(String));
1380
+ for (const id of [...dsmPendingPurged]) {
1381
+ if (dsmServerPurgedIds.has(id)) dsmPendingPurged.delete(id);
1382
+ }
1362
1383
  dsmAuthoritativeTitles = new Map(Object.entries(r && r.titles || {}).map(([id, title]) => [String(id), String(title)]));
1363
1384
  if (dsmRepaintDots) dsmRepaintDots();
1364
1385
  } catch (e) {
1365
1386
  }
1366
1387
  return dsmTrashIds;
1367
1388
  }
1368
- var DSM_KEY_PURGED = "dsm-purged-v1";
1369
- var dsmPurgedIds = null;
1389
+ var dsmPendingPurged = /* @__PURE__ */ new Set();
1370
1390
  function dsmLoadPurged() {
1371
- if (!dsmPurgedIds) {
1372
- try {
1373
- dsmPurgedIds = new Set(JSON.parse(localStorage.getItem(DSM_KEY_PURGED) || "[]"));
1374
- } catch (e) {
1375
- dsmPurgedIds = /* @__PURE__ */ new Set();
1376
- }
1377
- }
1378
- dsmServerPurgedIds.forEach((id) => dsmPurgedIds.add(id));
1379
- return dsmPurgedIds;
1380
- }
1381
- function dsmSavePurged() {
1382
- try {
1383
- localStorage.setItem(DSM_KEY_PURGED, JSON.stringify([...dsmLoadPurged()]));
1384
- } catch (e) {
1385
- }
1391
+ return /* @__PURE__ */ new Set([...dsmServerPurgedIds, ...dsmPendingPurged]);
1386
1392
  }
1387
1393
  function dsmMarkPurged(ids) {
1388
- const s = dsmLoadPurged();
1389
- ids.forEach((id) => s.add(String(id)));
1390
- dsmSavePurged();
1394
+ ids.forEach((id) => dsmPendingPurged.add(String(id)));
1391
1395
  if (dsmRepaintDots) dsmRepaintDots();
1392
1396
  }
1397
+ var SIDEBAR_ADAPTER_VERSION = 3;
1398
+ var SIDEBAR_ADAPTER_MAX_MISSES = 200;
1399
+ function createSidebarAdapter() {
1400
+ let disabled = false;
1401
+ let misses = 0;
1402
+ const findFiber = (el) => {
1403
+ const k = Object.keys(el).find((kk) => kk.startsWith("__reactFiber") || kk.startsWith("__reactInternalInstance"));
1404
+ return k ? el[k] : null;
1405
+ };
1406
+ const recognize = (row) => {
1407
+ const out = { node: null, group: null };
1408
+ if (disabled || !row || typeof row !== "object") return out;
1409
+ let f = findFiber(row);
1410
+ let guard = 0;
1411
+ while (f && guard++ < 300) {
1412
+ const props = f.memoizedProps;
1413
+ if (props && props.node && typeof props.node === "object") out.node = props.node;
1414
+ if (props && props.group && typeof props.group === "object") out.group = props.group;
1415
+ f = f.return;
1416
+ }
1417
+ if (!out.node && !out.group) {
1418
+ if (++misses >= SIDEBAR_ADAPTER_MAX_MISSES) {
1419
+ disabled = true;
1420
+ try {
1421
+ console.warn("[dsh-sessions-manager] \u4FA7\u680F\u6CE8\u5165 adapter v" + SIDEBAR_ADAPTER_VERSION + " \u5DF2\u505C\u7528\uFF1A\u672A\u8BC6\u522B\u5230\u5DF2\u77E5\u7684 DSH \u4FA7\u680F\u8282\u70B9\u7ED3\u6784\uFF08\u4E0A\u6E38\u53EF\u80FD\u5DF2\u6539\u7248\uFF09");
1422
+ } catch (e) {
1423
+ }
1424
+ }
1425
+ } else if (misses > 0) {
1426
+ misses = 0;
1427
+ }
1428
+ return out;
1429
+ };
1430
+ return {
1431
+ version: SIDEBAR_ADAPTER_VERSION,
1432
+ get disabled() {
1433
+ return disabled;
1434
+ },
1435
+ findFiber,
1436
+ recognize
1437
+ };
1438
+ }
1439
+ var sidebarAdapter = createSidebarAdapter();
1393
1440
  function installSidebarSessionMenuAug() {
1394
1441
  if (typeof document === "undefined") return;
1395
1442
  const AUG = "data-dsm-aug";
1396
1443
  let styleInjected = false;
1397
1444
  let activeSubClose = null;
1398
1445
  let hoverTimer = null;
1399
- const findFiber = (el) => {
1400
- const k = Object.keys(el).find((kk) => kk.startsWith("__reactFiber") || kk.startsWith("__reactInternalInstance"));
1401
- return k ? el[k] : null;
1402
- };
1403
1446
  const sessionInfoFromMenu = (menuEl) => {
1404
- let f = findFiber(menuEl);
1447
+ let f = sidebarAdapter.findFiber(menuEl);
1405
1448
  let guard = 0;
1406
1449
  while (f && guard++ < 300) {
1407
1450
  const p = f.memoizedProps;
@@ -1584,7 +1627,7 @@ function installSidebarSessionMenuAug() {
1584
1627
  const move = mk("\u79FB\u52A8\u4F1A\u8BDD", ICON_MOVE, false);
1585
1628
  const del = mk("\u5220\u9664\u4F1A\u8BDD", ICON_DEL, true);
1586
1629
  const mark = mk(dsmLoadManual().has(info.id) ? "\u6807\u8BB0\u5DF2\u8BFB" : "\u6807\u8BB0\u672A\u8BFB", ICON_UNREAD, false);
1587
- const moveCapability = dsmActionCapability("move");
1630
+ const moveCapability = dsmActionCapability("relocateSession") || dsmActionCapability("move");
1588
1631
  if (!moveCapability || !moveCapability.available) {
1589
1632
  move.btn.disabled = true;
1590
1633
  move.btn.title = moveCapability && moveCapability.reason || "\u6B63\u5728\u68C0\u67E5\u5F53\u524D\u7248\u672C\u7684\u79FB\u52A8\u80FD\u529B";
@@ -1623,6 +1666,7 @@ function installSidebarSessionMenuAug() {
1623
1666
  };
1624
1667
  const seen = /* @__PURE__ */ new WeakSet();
1625
1668
  const obs = new MutationObserver(() => {
1669
+ if (sidebarAdapter.disabled) return;
1626
1670
  const menus = document.querySelectorAll('body > [role="menu"]');
1627
1671
  menus.forEach((menuEl) => {
1628
1672
  if (seen.has(menuEl)) return;
@@ -1646,21 +1690,7 @@ function installSidebarStatusDots() {
1646
1690
  s.textContent = SIDEBAR_AUG_CSS;
1647
1691
  document.head.appendChild(s);
1648
1692
  }
1649
- const findFiber = (el) => {
1650
- const k = Object.keys(el).find((kk) => kk.startsWith("__reactFiber") || kk.startsWith("__reactInternalInstance"));
1651
- return k ? el[k] : null;
1652
- };
1653
- const fiberProp = (el, pred) => {
1654
- let f = findFiber(el);
1655
- let g = 0;
1656
- while (f && g++ < 300) {
1657
- const p = f.memoizedProps;
1658
- if (p && pred(p)) return pred(p);
1659
- f = f.return;
1660
- }
1661
- return null;
1662
- };
1663
- const rowNode = (row) => fiberProp(row, (p) => p.node && typeof p.node.id === "string" && p.node.id ? p.node : null);
1693
+ const rowNode = (row) => sidebarAdapter.recognize(row).node;
1664
1694
  const rowId = (row) => {
1665
1695
  const node = rowNode(row);
1666
1696
  return node ? node.id : null;
@@ -1675,7 +1705,7 @@ function installSidebarStatusDots() {
1675
1705
  // 蓝 手动标记未读
1676
1706
  running: "#EAB308",
1677
1707
  // 黄 工作中 (DSH running)
1678
- feedback: "var(--dsw-alias-state-warning-primary)",
1708
+ feedback: "var(--dsw-alias-state-warn-primary, #F59E0B)",
1679
1709
  // 琥珀 需用户反馈 (DSH warning)
1680
1710
  done: "var(--dsw-alias-state-success-primary)",
1681
1711
  // 绿 完成后未读 (DSH done)
@@ -1683,7 +1713,9 @@ function installSidebarStatusDots() {
1683
1713
  // 红 出错/需关注 (DSH error)
1684
1714
  };
1685
1715
  const manualUnread = dsmLoadManual();
1716
+ const titleInitializedRows = /* @__PURE__ */ new WeakSet();
1686
1717
  const paint = () => {
1718
+ if (sidebarAdapter.disabled) return;
1687
1719
  const activeId = activeRowId();
1688
1720
  if (activeId !== curActive && activeId && manualUnread.has(activeId)) {
1689
1721
  manualUnread.delete(activeId);
@@ -1703,12 +1735,20 @@ function installSidebarStatusDots() {
1703
1735
  }
1704
1736
  if (row.style.display === "none") row.style.display = "";
1705
1737
  const authoritativeTitle = dsmAuthoritativeTitles.get(id);
1706
- if (authoritativeTitle) {
1738
+ if (!titleInitializedRows.has(row)) {
1707
1739
  const node = rowNode(row) || {};
1708
1740
  const expected = new Set([node.title, node.displayTitle, node.name].filter((value) => typeof value === "string"));
1709
1741
  const spans = [...row.children].filter((el) => el.tagName === "SPAN" && !el.querySelector("[data-state]") && (el.textContent || "").trim());
1710
1742
  const titleEl = spans.find((el) => expected.has((el.textContent || "").trim())) || spans[0];
1711
- if (titleEl && titleEl.textContent !== authoritativeTitle) titleEl.textContent = authoritativeTitle;
1743
+ if (titleEl) {
1744
+ const correction = authoritativeTitleForFirstPaint({
1745
+ firstPaint: true,
1746
+ rendered: titleEl.textContent || "",
1747
+ authoritative: authoritativeTitle || ""
1748
+ });
1749
+ if (correction) titleEl.textContent = correction;
1750
+ titleInitializedRows.add(row);
1751
+ }
1712
1752
  }
1713
1753
  const sd = row.querySelector("[data-state]");
1714
1754
  if (sd) sd.style.display = "none";
@@ -1738,10 +1778,6 @@ function installSidebarStatusDots() {
1738
1778
  });
1739
1779
  };
1740
1780
  dsmRepaintDots = paint;
1741
- const tick = () => {
1742
- if (++dsmTrashTick % 8 === 0) dsmLoadTrashIds();
1743
- paint();
1744
- };
1745
1781
  let raf = 0;
1746
1782
  const schedulePaint = () => {
1747
1783
  if (raf) return;
@@ -1750,38 +1786,38 @@ function installSidebarStatusDots() {
1750
1786
  paint();
1751
1787
  });
1752
1788
  };
1789
+ const FALLBACK_TICK_MS = 4e3;
1790
+ const tick = () => {
1791
+ if (typeof document !== "undefined" && document.hidden) return;
1792
+ if (++dsmTrashTick % 8 === 0) dsmLoadTrashIds();
1793
+ paint();
1794
+ };
1753
1795
  tick();
1754
1796
  dsmLoadTrashIds();
1755
1797
  paint();
1756
1798
  const obs = new MutationObserver(schedulePaint);
1757
- obs.observe(document.body, { childList: true, subtree: true });
1758
- setInterval(tick, 1200);
1799
+ obs.observe(document.body, {
1800
+ childList: true,
1801
+ subtree: true,
1802
+ attributes: true,
1803
+ attributeFilter: ["data-state", "aria-selected", "class", "style"],
1804
+ characterData: true
1805
+ });
1806
+ setInterval(tick, FALLBACK_TICK_MS);
1759
1807
  }
1760
1808
  function installSidebarWorkspaceDrag() {
1761
1809
  if (typeof document === "undefined") return;
1762
1810
  let sessions = /* @__PURE__ */ new Map();
1763
1811
  let dragging = null;
1764
1812
  let moving = false;
1765
- const findFiber = (el) => {
1766
- const key = Object.keys(el).find((k) => k.startsWith("__reactFiber") || k.startsWith("__reactInternalInstance"));
1767
- return key ? el[key] : null;
1768
- };
1769
- const fiberNodes = (el) => {
1770
- const out = [];
1771
- let fiber = findFiber(el);
1772
- let guard = 0;
1773
- while (fiber && guard++ < 300) {
1774
- const props = fiber.memoizedProps;
1775
- if (props && props.node && typeof props.node === "object") out.push(props.node);
1776
- if (props && props.group && typeof props.group === "object") out.push(props.group);
1777
- fiber = fiber.return;
1778
- }
1779
- return out;
1813
+ const sessionForRow = (row) => {
1814
+ const { node, group } = sidebarAdapter.recognize(row);
1815
+ return sessionForNodes(group ? [node, group].filter(Boolean) : [node], sessions);
1780
1816
  };
1781
- const sessionForRow = (row) => sessionForNodes(fiberNodes(row), sessions);
1782
1817
  const workspaceForRow = (row) => {
1783
1818
  if (sessionForRow(row)) return null;
1784
- return workspaceForNodes(fiberNodes(row));
1819
+ const { group } = sidebarAdapter.recognize(row);
1820
+ return group ? workspaceForNodes([group]) : null;
1785
1821
  };
1786
1822
  const eventRow = (event) => {
1787
1823
  for (const item of event.composedPath ? event.composedPath() : []) {
@@ -1801,6 +1837,7 @@ function installSidebarWorkspaceDrag() {
1801
1837
  document.querySelectorAll(".dsm-drag-source,.dsm-drop-target").forEach((el) => el.classList.remove("dsm-drag-source", "dsm-drop-target"));
1802
1838
  };
1803
1839
  const decorateRows = () => {
1840
+ if (sidebarAdapter.disabled) return;
1804
1841
  document.querySelectorAll('[role="treeitem"]').forEach((row) => {
1805
1842
  const session = sessionForRow(row);
1806
1843
  const ws = workspaceForRow(row);
@@ -1825,7 +1862,8 @@ function installSidebarWorkspaceDrag() {
1825
1862
  maybeRefresh();
1826
1863
  }, true);
1827
1864
  document.addEventListener("dragstart", (event) => {
1828
- const moveCapability = dsmActionCapability("move");
1865
+ if (sidebarAdapter.disabled) return;
1866
+ const moveCapability = dsmActionCapability("relocateSession") || dsmActionCapability("move");
1829
1867
  if (!moveCapability || !moveCapability.available) return;
1830
1868
  const row = eventRow(event);
1831
1869
  const item = row && sessionForRow(row);