dsh-sessions-manager 3.5.1 → 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
@@ -162,6 +162,16 @@ lib/client.js pre-built client (ModuleLoader CJS handshake)
162
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
163
  - Unverified future runtimes expose only capabilities the plugin can safely identify; method presence alone is not presented as behavioral compatibility.
164
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
+
165
175
  ## License
166
176
 
167
177
  [MIT](./LICENSE) © TOBYCAI
package/README.md CHANGED
@@ -162,6 +162,16 @@ lib/client.js 预构建 client(ModuleLoader CJS handshake)
162
162
  - `0.1.3-alpha.1`:支持 snapshot 列表和 `SessionHandle` 只读流程;永久删除与跨工作区移动在缺少已验证安全路径时自动禁用,其余管理能力继续工作。能力以面板实际提示为准。
163
163
  - 未经验证的未来 Runtime 默认只开放能够识别的安全能力;插件不会用方法存在与否冒充行为兼容。
164
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
+
165
175
  ## License
166
176
 
167
177
  [MIT](./LICENSE) © TOBYCAI
package/lib/client.js CHANGED
@@ -44,6 +44,8 @@ function dotStateFor({ manualUnread = false, dataState = "", isActive = false }
44
44
  case "running":
45
45
  return "running";
46
46
  case "warning":
47
+ case "waiting":
48
+ case "needs-attention":
47
49
  return "feedback";
48
50
  case "error":
49
51
  return "error";
@@ -96,12 +98,12 @@ function loadPanelPrefs() {
96
98
  }
97
99
  }
98
100
  var CSS = `
99
- .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}
100
102
  .archv-head{display:flex;align-items:center;gap:10px;margin:0 0 2px}
101
103
  .archv-title{font-size:16px;font-weight:650;color:var(--dsw-alias-label-primary);letter-spacing:-0.01em;margin:0}
102
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}
103
105
  .archv-sub{font-size:12px;line-height:1.55;color:var(--dsw-alias-label-tertiary);margin:0 0 12px;max-width:64ch}
104
- .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}
105
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}
106
108
  .sess-filter{display:flex;align-items:center;gap:6px;margin:0 0 12px}
107
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}
@@ -110,13 +112,13 @@ var CSS = `
110
112
  .sess-tools{display:grid;grid-template-columns:minmax(180px,1fr) minmax(140px,.7fr) minmax(130px,.55fr);gap:8px;margin:0 0 8px}
111
113
  .sess-field{display:flex;flex-direction:column;gap:5px;min-width:0}
112
114
  .sess-field label{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary)}
113
- .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}
114
116
  .sess-results{font-size:11px;color:var(--dsw-alias-label-tertiary);margin:0 0 4px}
115
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}
116
118
  .sess-batch{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 2px 4px;margin-bottom:4px}
117
119
  .sess-btntext{font-size:12px;color:var(--dsw-alias-label-tertiary);flex:none}
118
120
  .archv-list{display:flex;flex-direction:column;gap:8px}
119
- .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}
120
122
  .archv-card:hover{border-color:var(--dsw-alias-border-l4)}
121
123
  .archv-card-exp{border-color:var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1)}
122
124
  .archv-row{display:flex;align-items:center;gap:14px;width:100%;min-width:0}
@@ -137,16 +139,16 @@ var CSS = `
137
139
  .archv-star-on svg{fill:currentColor}
138
140
  .archv-body{flex:1;min-width:0;display:flex;align-items:center;gap:12px}
139
141
  .archv-actions{display:flex;gap:8px;flex:none;flex-wrap:nowrap;justify-content:flex-end}
140
- .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}
141
143
  .archv-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
142
144
  .archv-btn:disabled{opacity:.5;cursor:default}
143
145
  .archv-del{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 45%,transparent)}
144
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)}
145
147
  .archv-go{color:var(--dsw-alias-state-business-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 45%,transparent)}
146
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)}
147
- .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}
148
150
  .archv-skel{display:flex;flex-direction:column;gap:8px}
149
- .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}
150
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}
151
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)}
152
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}
@@ -155,24 +157,24 @@ var CSS = `
155
157
  @keyframes archv-pop{from{opacity:0;transform:translateX(-50%) translateY(10px)}}
156
158
  @media (prefers-reduced-motion:reduce){.archv-skel-card::after{animation:none}.archv-card,.archv-btn{transition:none}.archv-status,.archv-spin{animation:none}}
157
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}}
158
- .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)}
159
161
  .mv-sheet-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
160
162
  .mv-sheet-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}
161
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}
162
164
  .mv-sheet-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
163
- .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%}
164
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}
165
167
  .mv-segbtn:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
166
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)}
167
169
  .mv-field{display:flex;flex-direction:column;gap:6px}
168
170
  .mv-field label.mv-field-label{font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary)}
169
- .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}
170
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}
171
173
  .mv-browse-row{display:flex;align-items:center;gap:8px}
172
174
  .mv-browse-row input[type=text]{flex:1;min-width:0}
173
175
  .mv-foot{display:flex;justify-content:flex-end;align-items:center;gap:8px;margin-top:2px}
174
176
  @media (max-width:640px){.archv-row{flex-wrap:wrap}.mv-sheet{padding:12px}}
175
- .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)}
176
178
  .dtl-sheet-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
177
179
  .dtl-sheet-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}
178
180
  .dtl-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:8px;margin-top:10px}
@@ -193,7 +195,7 @@ var CSS = `
193
195
  .more-wrap{position:relative;flex:none}
194
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}
195
197
  .more-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
196
- .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}
197
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}
198
200
  .more-item:hover{background:var(--dsw-alias-interactive-bg-hover)}
199
201
  .more-item-danger{color:var(--dsw-alias-state-error-primary)}
@@ -436,9 +438,13 @@ function SessionPanel({ workspacesSvc }) {
436
438
  const w = workspaces.find((x) => x.workspaceId === id);
437
439
  return w ? w.path : "";
438
440
  };
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");
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");
442
448
  const archivedList = sessions ? sessions.filter((x) => x.archived) : [];
443
449
  const activeList = sessions ? sessions.filter((x) => !x.archived) : [];
444
450
  const starredList = starredOf(sessions);
@@ -579,9 +585,9 @@ function SessionPanel({ workspacesSvc }) {
579
585
  const restoreTrash = (sid) => {
580
586
  if (trashBusy) return;
581
587
  setTrashBusy(sid);
582
- postJSON("/archived-sessions/trash/restore", { sessionId: sid }).then(() => {
588
+ postJSON("/archived-sessions/trash/restore", { sessionId: sid }).then((r) => {
583
589
  setTrashBusy(null);
584
- 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");
585
591
  loadTrash();
586
592
  refresh();
587
593
  }).catch((e) => {
@@ -1217,10 +1223,16 @@ function SessionPanel({ workspacesSvc }) {
1217
1223
  ] }),
1218
1224
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "sess-field", children: [
1219
1225
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { children: "\u6C38\u4E45\u6E05\u7406" }),
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" })
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" })
1221
1227
  ] })
1222
1228
  ] }),
1223
- !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" }),
1224
1236
  trashCheck && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: trashCheck.missing ? "archv-err" : "archv-empty", role: "status", children: [
1225
1237
  "\u6821\u9A8C\u5B8C\u6210\uFF1A",
1226
1238
  trashCheck.healthy,
@@ -1234,7 +1246,7 @@ function SessionPanel({ workspacesSvc }) {
1234
1246
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsm-trash-name", title: t.sessionId, children: t.title || t.sessionId }),
1235
1247
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsm-trash-date", children: fmtDate(t.deletedAt) }),
1236
1248
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dsm-trash-actions", children: [
1237
- /* @__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" }),
1238
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" })
1239
1251
  ] })
1240
1252
  ] }, t.sessionId)) })
@@ -1245,7 +1257,7 @@ function SessionPanel({ workspacesSvc }) {
1245
1257
  purgeTarget === "__all" ? "\u5168\u90E8\u56DE\u6536\u7AD9\u4F1A\u8BDD" : "\u8FD9\u4E2A\u4F1A\u8BDD",
1246
1258
  "\uFF1F"
1247
1259
  ] }),
1248
- /* @__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" }),
1249
1261
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dlg-actions", children: [
1250
1262
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn", onClick: () => setPurgeTarget(null), children: "\u53D6\u6D88" }),
1251
1263
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn archv-del", onClick: () => {
@@ -1356,57 +1368,83 @@ async function dsmLoadCapabilities() {
1356
1368
  }
1357
1369
  return dsmCapabilities;
1358
1370
  }
1359
- function dsmActionCapability(name) {
1360
- 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;
1361
1374
  }
1362
1375
  async function dsmLoadTrashIds() {
1363
1376
  try {
1364
1377
  const r = await postJSON("/archived-sessions/sidebar-state", {});
1365
1378
  dsmTrashIds = new Set((r && r.trashedSessionIds || []).map(String));
1366
1379
  dsmServerPurgedIds = new Set((r && r.purgedSessionIds || []).map(String));
1380
+ for (const id of [...dsmPendingPurged]) {
1381
+ if (dsmServerPurgedIds.has(id)) dsmPendingPurged.delete(id);
1382
+ }
1367
1383
  dsmAuthoritativeTitles = new Map(Object.entries(r && r.titles || {}).map(([id, title]) => [String(id), String(title)]));
1368
1384
  if (dsmRepaintDots) dsmRepaintDots();
1369
1385
  } catch (e) {
1370
1386
  }
1371
1387
  return dsmTrashIds;
1372
1388
  }
1373
- var DSM_KEY_PURGED = "dsm-purged-v1";
1374
- var dsmPurgedIds = null;
1389
+ var dsmPendingPurged = /* @__PURE__ */ new Set();
1375
1390
  function dsmLoadPurged() {
1376
- if (!dsmPurgedIds) {
1377
- try {
1378
- dsmPurgedIds = new Set(JSON.parse(localStorage.getItem(DSM_KEY_PURGED) || "[]"));
1379
- } catch (e) {
1380
- dsmPurgedIds = /* @__PURE__ */ new Set();
1381
- }
1382
- }
1383
- dsmServerPurgedIds.forEach((id) => dsmPurgedIds.add(id));
1384
- return dsmPurgedIds;
1385
- }
1386
- function dsmSavePurged() {
1387
- try {
1388
- localStorage.setItem(DSM_KEY_PURGED, JSON.stringify([...dsmLoadPurged()]));
1389
- } catch (e) {
1390
- }
1391
+ return /* @__PURE__ */ new Set([...dsmServerPurgedIds, ...dsmPendingPurged]);
1391
1392
  }
1392
1393
  function dsmMarkPurged(ids) {
1393
- const s = dsmLoadPurged();
1394
- ids.forEach((id) => s.add(String(id)));
1395
- dsmSavePurged();
1394
+ ids.forEach((id) => dsmPendingPurged.add(String(id)));
1396
1395
  if (dsmRepaintDots) dsmRepaintDots();
1397
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();
1398
1440
  function installSidebarSessionMenuAug() {
1399
1441
  if (typeof document === "undefined") return;
1400
1442
  const AUG = "data-dsm-aug";
1401
1443
  let styleInjected = false;
1402
1444
  let activeSubClose = null;
1403
1445
  let hoverTimer = null;
1404
- const findFiber = (el) => {
1405
- const k = Object.keys(el).find((kk) => kk.startsWith("__reactFiber") || kk.startsWith("__reactInternalInstance"));
1406
- return k ? el[k] : null;
1407
- };
1408
1446
  const sessionInfoFromMenu = (menuEl) => {
1409
- let f = findFiber(menuEl);
1447
+ let f = sidebarAdapter.findFiber(menuEl);
1410
1448
  let guard = 0;
1411
1449
  while (f && guard++ < 300) {
1412
1450
  const p = f.memoizedProps;
@@ -1589,7 +1627,7 @@ function installSidebarSessionMenuAug() {
1589
1627
  const move = mk("\u79FB\u52A8\u4F1A\u8BDD", ICON_MOVE, false);
1590
1628
  const del = mk("\u5220\u9664\u4F1A\u8BDD", ICON_DEL, true);
1591
1629
  const mark = mk(dsmLoadManual().has(info.id) ? "\u6807\u8BB0\u5DF2\u8BFB" : "\u6807\u8BB0\u672A\u8BFB", ICON_UNREAD, false);
1592
- const moveCapability = dsmActionCapability("move");
1630
+ const moveCapability = dsmActionCapability("relocateSession") || dsmActionCapability("move");
1593
1631
  if (!moveCapability || !moveCapability.available) {
1594
1632
  move.btn.disabled = true;
1595
1633
  move.btn.title = moveCapability && moveCapability.reason || "\u6B63\u5728\u68C0\u67E5\u5F53\u524D\u7248\u672C\u7684\u79FB\u52A8\u80FD\u529B";
@@ -1628,6 +1666,7 @@ function installSidebarSessionMenuAug() {
1628
1666
  };
1629
1667
  const seen = /* @__PURE__ */ new WeakSet();
1630
1668
  const obs = new MutationObserver(() => {
1669
+ if (sidebarAdapter.disabled) return;
1631
1670
  const menus = document.querySelectorAll('body > [role="menu"]');
1632
1671
  menus.forEach((menuEl) => {
1633
1672
  if (seen.has(menuEl)) return;
@@ -1651,21 +1690,7 @@ function installSidebarStatusDots() {
1651
1690
  s.textContent = SIDEBAR_AUG_CSS;
1652
1691
  document.head.appendChild(s);
1653
1692
  }
1654
- const findFiber = (el) => {
1655
- const k = Object.keys(el).find((kk) => kk.startsWith("__reactFiber") || kk.startsWith("__reactInternalInstance"));
1656
- return k ? el[k] : null;
1657
- };
1658
- const fiberProp = (el, pred) => {
1659
- let f = findFiber(el);
1660
- let g = 0;
1661
- while (f && g++ < 300) {
1662
- const p = f.memoizedProps;
1663
- if (p && pred(p)) return pred(p);
1664
- f = f.return;
1665
- }
1666
- return null;
1667
- };
1668
- 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;
1669
1694
  const rowId = (row) => {
1670
1695
  const node = rowNode(row);
1671
1696
  return node ? node.id : null;
@@ -1690,6 +1715,7 @@ function installSidebarStatusDots() {
1690
1715
  const manualUnread = dsmLoadManual();
1691
1716
  const titleInitializedRows = /* @__PURE__ */ new WeakSet();
1692
1717
  const paint = () => {
1718
+ if (sidebarAdapter.disabled) return;
1693
1719
  const activeId = activeRowId();
1694
1720
  if (activeId !== curActive && activeId && manualUnread.has(activeId)) {
1695
1721
  manualUnread.delete(activeId);
@@ -1752,10 +1778,6 @@ function installSidebarStatusDots() {
1752
1778
  });
1753
1779
  };
1754
1780
  dsmRepaintDots = paint;
1755
- const tick = () => {
1756
- if (++dsmTrashTick % 8 === 0) dsmLoadTrashIds();
1757
- paint();
1758
- };
1759
1781
  let raf = 0;
1760
1782
  const schedulePaint = () => {
1761
1783
  if (raf) return;
@@ -1764,38 +1786,38 @@ function installSidebarStatusDots() {
1764
1786
  paint();
1765
1787
  });
1766
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
+ };
1767
1795
  tick();
1768
1796
  dsmLoadTrashIds();
1769
1797
  paint();
1770
1798
  const obs = new MutationObserver(schedulePaint);
1771
- obs.observe(document.body, { childList: true, subtree: true });
1772
- 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);
1773
1807
  }
1774
1808
  function installSidebarWorkspaceDrag() {
1775
1809
  if (typeof document === "undefined") return;
1776
1810
  let sessions = /* @__PURE__ */ new Map();
1777
1811
  let dragging = null;
1778
1812
  let moving = false;
1779
- const findFiber = (el) => {
1780
- const key = Object.keys(el).find((k) => k.startsWith("__reactFiber") || k.startsWith("__reactInternalInstance"));
1781
- return key ? el[key] : null;
1782
- };
1783
- const fiberNodes = (el) => {
1784
- const out = [];
1785
- let fiber = findFiber(el);
1786
- let guard = 0;
1787
- while (fiber && guard++ < 300) {
1788
- const props = fiber.memoizedProps;
1789
- if (props && props.node && typeof props.node === "object") out.push(props.node);
1790
- if (props && props.group && typeof props.group === "object") out.push(props.group);
1791
- fiber = fiber.return;
1792
- }
1793
- return out;
1813
+ const sessionForRow = (row) => {
1814
+ const { node, group } = sidebarAdapter.recognize(row);
1815
+ return sessionForNodes(group ? [node, group].filter(Boolean) : [node], sessions);
1794
1816
  };
1795
- const sessionForRow = (row) => sessionForNodes(fiberNodes(row), sessions);
1796
1817
  const workspaceForRow = (row) => {
1797
1818
  if (sessionForRow(row)) return null;
1798
- return workspaceForNodes(fiberNodes(row));
1819
+ const { group } = sidebarAdapter.recognize(row);
1820
+ return group ? workspaceForNodes([group]) : null;
1799
1821
  };
1800
1822
  const eventRow = (event) => {
1801
1823
  for (const item of event.composedPath ? event.composedPath() : []) {
@@ -1815,6 +1837,7 @@ function installSidebarWorkspaceDrag() {
1815
1837
  document.querySelectorAll(".dsm-drag-source,.dsm-drop-target").forEach((el) => el.classList.remove("dsm-drag-source", "dsm-drop-target"));
1816
1838
  };
1817
1839
  const decorateRows = () => {
1840
+ if (sidebarAdapter.disabled) return;
1818
1841
  document.querySelectorAll('[role="treeitem"]').forEach((row) => {
1819
1842
  const session = sessionForRow(row);
1820
1843
  const ws = workspaceForRow(row);
@@ -1839,7 +1862,8 @@ function installSidebarWorkspaceDrag() {
1839
1862
  maybeRefresh();
1840
1863
  }, true);
1841
1864
  document.addEventListener("dragstart", (event) => {
1842
- const moveCapability = dsmActionCapability("move");
1865
+ if (sidebarAdapter.disabled) return;
1866
+ const moveCapability = dsmActionCapability("relocateSession") || dsmActionCapability("move");
1843
1867
  if (!moveCapability || !moveCapability.available) return;
1844
1868
  const row = eventRow(event);
1845
1869
  const item = row && sessionForRow(row);