dsh-workspace-mover 2.3.0 → 2.4.0
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/CHANGELOG.md +26 -0
- package/README.md +14 -5
- package/README_EN.md +12 -5
- package/client/client.js +80 -15
- package/docs/ARCHITECTURE.md +224 -214
- package/docs/compatibility/dsh-0.2.1-rc.2-2026-10-18.md +37 -0
- package/docs/compatibility/issue-5-desktop-acceptance-2026-10-22.md +106 -0
- package/docs/compatibility/pr-review-2026-10-22.md +56 -0
- package/lib/index.js +4507 -4263
- package/package.json +3 -2
- package/test/client-dom.test.mjs +79 -0
- package/test/e2e-sandbox.test.mjs +2724 -2607
- package/test/host-services.test.mjs +303 -0
- package/test/installed-host.test.mjs +119 -0
- package/test/workspace-tools.test.mjs +977 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.4.0] - 2026-10-22
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Agent tools mover_create_workspace (register an existing folder) and mover_rename_workspace (display title only), from PR #7. Duplicate names/folder aliases are rejected. Rename asks for approval unless the calling session has full access.
|
|
8
|
+
- Sidebar edge autoscroll from PR #9, with pointer bounds, scroll limits, drag cancellation and hot-unload cleanup.
|
|
9
|
+
- pluginVersion in status and doctor output.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Issue #5: also detect opening/closing JSONL writer claims even after the session leaves memory. Unknown handle backends wait safely. Keep header rewrite and relocation synchronous after the final writer check.
|
|
14
|
+
- Fresh-install scans treat only missing storage roots as empty (PR #8); permission and I/O failures remain visible.
|
|
15
|
+
- Optional tools/approval no longer disable the whole plugin. Tools follow host service load/unload; approval requests carry ACP callId. Missing approval or agent context refuses operations requiring confirmation.
|
|
16
|
+
|
|
17
|
+
- Queue resident sessions until unloading instead of mutating private writer state or frozen live headers; await backend flush before cold migration. Workspace repoint refuses resident writers before changing registry paths.
|
|
18
|
+
- Preserve existing working-directory history and append the destination directory when needed; invalidate the corresponding projection checkpoint. Unsupported sequences are rejected before migration.
|
|
19
|
+
- Restore v3/v4 backup filenames from the official header version; preserve unversioned legacy filenames.
|
|
20
|
+
- Read Portable Bundle pluginVersion from package.json.
|
|
21
|
+
|
|
22
|
+
### Validation
|
|
23
|
+
|
|
24
|
+
- Desktop `0.2.1-rc.2`: user accepted candidate `2.4.0-rc.1`; opened-session queueing, continued A2 replies while pending, migration after restart, B1/B2 replies across a second restart, destination working directory and doctor checks passed.
|
|
25
|
+
- Workspace registration, rename rejection/approval, name conflicts, no-op renames and full-access rename passed in Desktop.
|
|
26
|
+
- Windows local gate: 315 tests passed; installed official JSONL/ToolRuntime backend check passed. Sidebar autoscroll is covered by automated DOM checks; the user waived manual autoscroll acceptance.
|
|
27
|
+
- Open sessions finish migration only after their writer is unloaded or DSH is restarted. Switch to another session before restarting so Desktop does not immediately reopen the pending session.
|
|
28
|
+
|
|
3
29
|
## [2.3.0] - 2026-10-13
|
|
4
30
|
|
|
5
31
|
### Added
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# dsh-workspace-mover
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
当前版本:2.4.0。[Desktop 验收记录](docs/compatibility/issue-5-desktop-acceptance-2026-10-22.md)。
|
|
5
|
+
|
|
6
|
+
当前待发布修复:运行中或仍驻留内存的会话会进入等待队列。DSH 暂无安全的写入句柄重绑定接口,因此驻留会话需要卸载或重启后才能迁移,单纯变为空闲并不足够。迁移会同步会话的当前工作目录到目标工作区;已有目录变更历史会保留,并追加一条目录变更记录。详见[验证记录与检验方法](docs/compatibility/dsh-0.2.1-rc.2-2026-10-18.md)。
|
|
3
7
|
> [!IMPORTANT]
|
|
4
8
|
> 非官方项目,由社区成员独立开发和维护。
|
|
5
9
|
|
|
@@ -61,10 +65,10 @@ DeepSeek Harness 侧边栏可以拖拽排序**同一工作区**内的会话,
|
|
|
61
65
|
- **🛡️ 数据保护**:相同会话内容只保留一份物理快照,往返迁移不会无限增长;每次迁移仍写入历史,备份列表显示快照份数与占用
|
|
62
66
|
- **⏪ 移动历史与撤回**:记录最近 100 次跨工作区移动,批量聚合为一条,整批可一键撤回
|
|
63
67
|
- **🧾 迁移任务中心**:批量迁移逐项持久化记录(完成/失败、最后错误与尝试时间),失败项一键重试——按会话当前位置重新迁移,不用陈旧路径
|
|
64
|
-
- **⏳
|
|
68
|
+
- **⏳ 忙碌会话自动排队**:运行中或写入句柄未释放的会话不会触碰文件,卸载或重启后自动重试;队列持久化,支持单项取消、单项重试和批量重试失败项
|
|
65
69
|
- **🔎 迁移预览**:确认前展示来源、目标路径、冲突、运行状态、适配器和本次预计新增备份大小;已有内容快照会明确显示复用
|
|
66
70
|
- **📂 空分组清理 / 打开文件夹**:只列出真正零成员的工作区;组菜单可直达系统文件管理器
|
|
67
|
-
- **🤖 Agent 工具**:`mover_list_sessions` / `mover_move_session` / `mover_repair_sessions` / `mover_doctor`
|
|
71
|
+
- **🤖 Agent 工具**:`mover_list_sessions` / `mover_move_session` / `mover_repair_sessions` / `mover_doctor` / `mover_create_workspace` / `mover_rename_workspace` 六个模型可调用工具——在对话里说「把这个会话挪到某组」即可触发与面板完全相同的备份回滚管线,也能新建、改名工作区;迁移、修复与改名执行前经宿主审批弹窗确认(改名在完全访问模式下免确认),未装 dsh-tools 的宿主自动停用
|
|
68
72
|
|
|
69
73
|
## 🚀 安装
|
|
70
74
|
|
|
@@ -176,10 +180,14 @@ dsh plugin --profile web add "link:C:/path/to/dsh-workspace-mover"
|
|
|
176
180
|
|
|
177
181
|
### 对话里迁移(Agent 工具)
|
|
178
182
|
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
需要审批的工具在审批服务缺失时会拒绝执行,可改用设置中的会话修复面板。
|
|
184
|
+
|
|
185
|
+
1. 装有 `dsh-tools` 的宿主(web 与桌面版默认)自动注册六个模型工具:会话清单(只读)、迁移、一键修复、自检(只读)、新建工作区、工作区改名;
|
|
186
|
+
2. 在对话里说「列出我的会话」「把会话 X 挪到 Y 组」「跑一次修复」「自检一下」「用文件夹 /路径 新建一个叫 Z 的工作区」「把 Y 组改名为 Z」——模型先用清单工具把名字解析成精确 id,再执行动作;
|
|
181
187
|
3. 迁移与修复在动手前经宿主审批弹窗请你确认(与面板确认同级);拒绝或关闭弹窗则不执行;两个只读工具(清单 / 自检)不弹确认;
|
|
182
|
-
4.
|
|
188
|
+
4. **新建工作区**:把一个已存在的文件夹登记为新分组;该文件夹(含指向它的符号链接)或该名称已被其他工作区使用时拒绝,不会在磁盘上创建文件夹,不弹确认;
|
|
189
|
+
5. **工作区改名**:只改显示名,执行前弹窗确认;会话处于**完全访问**(`danger-full-access`)时免确认;与其他工作区重名时拒绝;
|
|
190
|
+
6. 宿主没有 `dsh-tools` 时自动停用,不影响面板与拖拽。
|
|
183
191
|
|
|
184
192
|
### 批量迁移
|
|
185
193
|
|
|
@@ -228,6 +236,7 @@ dsh plugin --profile web add "link:C:/path/to/dsh-workspace-mover"
|
|
|
228
236
|
|
|
229
237
|
| 插件版本 | 已验证 DeepSeek Harness | Node | 备注 |
|
|
230
238
|
|----------|-------------------------|------|------|
|
|
239
|
+
| 2.4.0 | `0.2.1-rc.2` Desktop | `≥22` | 用户确认排队、迁移前后继续发送、两次重启、目标工作目录和工作区工具审批;自动滚动仅自动检查 |
|
|
231
240
|
| 2.3.0 | `0.2.0-rc.2` Desktop(用户验证部分场景) | `≥22` | 空闲会话迁移与快照复用已反馈通过;Web、队列进程重启和完整 runtime smoke 待验证 |
|
|
232
241
|
| 2.2.0 | `0.1.5-rc.1`、`0.1.5-rc.2`、**`0.2.0 桌面版`**(0.2.0-rc.2 内核) | `≥22` | 存储布局自检、Agent 工具与桌面版实测;宿主缺 `dsh-tools` 时工具自动停用 |
|
|
233
242
|
| 2.0.3 | `0.1.5-rc.1`、`0.1.5-rc.2` | `≥22` | 真迁移 / 批量 / 救援 / 回收站 / 备份;首次发布 npm |
|
package/README_EN.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# dsh-workspace-mover
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
Running or resident sessions wait in the durable queue. Idle alone does not release a backend writer; unload the session or restart DSH before migration. Migration sets the current working directory to the target workspace, preserving existing directory-change events and appending a final change when needed. See the [validation record](docs/compatibility/dsh-0.2.1-rc.2-2026-10-18.md).
|
|
3
5
|
> [!IMPORTANT]
|
|
4
6
|
> Unofficial project, independently developed and maintained by community members.
|
|
5
7
|
|
|
@@ -65,7 +67,7 @@ DeepSeek Harness's sidebar supports drag-to-reorder **within** a workspace, but
|
|
|
65
67
|
- **⏳ Busy-session queue**: running sessions are never touched; queued work is persistent, with per-item cancel/retry and batch retry for failed items
|
|
66
68
|
- **🔎 Migration preview**: before confirmation, show source, target path, collisions, running state, adapter mode, and estimated new backup bytes; reuse is called out when a matching snapshot already exists
|
|
67
69
|
- **📂 Empty-group cleanup / open folder**: only truly empty workspaces are listed; open a group directory in your file manager
|
|
68
|
-
- **🤖 Agent tools**:
|
|
70
|
+
- **🤖 Agent tools**: six model-callable tools — `mover_list_sessions` / `mover_move_session` / `mover_repair_sessions` / `mover_doctor` / `mover_create_workspace` / `mover_rename_workspace` — so "move this session to group X" in the conversation runs the same backup-first pipeline as the panel, and workspaces can be created and renamed too; move, repair and rename ask for user approval first (rename skips it under full access), and hosts without `dsh-tools` skip them automatically
|
|
69
71
|
|
|
70
72
|
## 🚀 Install
|
|
71
73
|
|
|
@@ -177,10 +179,14 @@ Mid-turn sessions are rejected; failed moves roll back automatically.
|
|
|
177
179
|
|
|
178
180
|
### Conversational moves (agent tools)
|
|
179
181
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
Operations requiring approval are refused when the approval service is absent; use the rescue panel in settings instead.
|
|
183
|
+
|
|
184
|
+
1. Hosts with `dsh-tools` (web and desktop by default) register six model tools: listing and doctor (both read-only), move, one-click repair, create workspace, and rename workspace;
|
|
185
|
+
2. Say "list my sessions", "move session X to group Y", "run a repair", "run a self-check", "create a workspace called Z for folder /path", or "rename group Y to Z" — the model resolves names into exact ids with the listing tool first, then acts;
|
|
186
|
+
3. Move and repair tools raise a host approval prompt before acting (same level as the panel confirm); dismissing or denying means nothing happens; the two read-only tools never prompt;
|
|
187
|
+
4. **Create workspace** registers an existing folder under a new name. It is refused if another workspace already uses that folder (including a symlink to it) or that name, and it never creates folders on disk. It does not ask for approval;
|
|
188
|
+
5. **Rename workspace** changes only the display name and asks for approval first. Sessions with **full access** (`danger-full-access`) rename without asking. A name another workspace already uses is refused;
|
|
189
|
+
6. Hosts without `dsh-tools` skip the tools automatically — the panel and drag flows are unaffected.
|
|
184
190
|
|
|
185
191
|
### Bulk move
|
|
186
192
|
|
|
@@ -231,6 +237,7 @@ Recovery snapshots are separate from primary archives. A reused snapshot retains
|
|
|
231
237
|
|
|
232
238
|
| Plugin version | Verified DeepSeek Harness | Node | Notes |
|
|
233
239
|
|----------------|---------------------------|------|-------|
|
|
240
|
+
| 2.4.0 | `0.2.1-rc.2` Desktop | `≥22` | User accepted queueing, continued replies, two restarts, target cwd and workspace-tool approval; autoscroll automated checks only |
|
|
234
241
|
| 2.3.0 | `0.2.0-rc.2` Desktop (partial user verification) | `≥22` | Idle migration and snapshot reuse confirmed; Web, queue process restart, and full runtime smoke pending |
|
|
235
242
|
| 2.2.0 | `0.1.5-rc.1`, `0.1.5-rc.2`, **`0.2.0 desktop`** (0.2.0-rc.2 core) | `≥22` | Storage-layout check; Agent tools + desktop verified; tools auto-skip on hosts without `dsh-tools` |
|
|
236
243
|
| 2.0.3 | `0.1.5-rc.1`, `0.1.5-rc.2` | `≥22` | True move / bulk / rescue / recycle bin / backups; first npm publish |
|
package/client/client.js
CHANGED
|
@@ -174,15 +174,15 @@ window.__ModuleLoader__.load({
|
|
|
174
174
|
tasksCaption: "迁移任务",
|
|
175
175
|
tasksHelp: "批量迁移的持久化记录:完成 / 失败逐项可查。失败项可一键重试——按会话当前位置重新迁移,不会用记录里的旧路径;清除记录不影响已迁移的会话。",
|
|
176
176
|
queueCaption: "等待迁移",
|
|
177
|
-
queueHelp: "
|
|
177
|
+
queueHelp: "运行中或仍驻留内存的会话暂不移动。停止生成后,若仍驻留,需等待会话卸载或重启;队列随后自动重试,失败项可重试或取消。",
|
|
178
178
|
queueRetryBtn: "重试",
|
|
179
179
|
queueRetryAllBtn: "重试全部失败项",
|
|
180
180
|
queueCancelBtn: "取消",
|
|
181
181
|
queueRetryDone: "✓ 已重新尝试迁移",
|
|
182
182
|
queueRetryAllDone: "✓ 已逐项重试 {n} 个失败任务",
|
|
183
183
|
queueCancelled: "✓ 已取消迁移任务",
|
|
184
|
-
queuedMove: "
|
|
185
|
-
batchQueuedTail: ";{n}
|
|
184
|
+
queuedMove: "已排队:等待会话停止运行并释放驻留写入句柄;可能需要重启 DSH",
|
|
185
|
+
batchQueuedTail: ";{n} 个会话已排队等待安全迁移",
|
|
186
186
|
previewStatus: "预览状态",
|
|
187
187
|
previewSource: "来源",
|
|
188
188
|
previewTarget: "目标路径",
|
|
@@ -203,7 +203,7 @@ window.__ModuleLoader__.load({
|
|
|
203
203
|
forkHint: "默认只迁移当前会话;选择后代分支会逐项迁移并独立记录结果。",
|
|
204
204
|
previewWarning: "提示",
|
|
205
205
|
previewSafe: "可安全移动",
|
|
206
|
-
previewQueued: "
|
|
206
|
+
previewQueued: "会进入迁移等待队列(可能需重启)",
|
|
207
207
|
previewBlocked: "当前不可移动",
|
|
208
208
|
previewWarningState: "需要确认",
|
|
209
209
|
taskRetryBtn: "重试失败项",
|
|
@@ -374,15 +374,15 @@ window.__ModuleLoader__.load({
|
|
|
374
374
|
tasksCaption: "Move tasks",
|
|
375
375
|
tasksHelp: "Persistent records of bulk moves: every done / failed item is inspectable. Failed items can be retried in one click — retried from where each session currently lives, never from a stale recorded path; clearing a record never touches moved sessions.",
|
|
376
376
|
queueCaption: "Waiting migrations",
|
|
377
|
-
queueHelp: "Running
|
|
377
|
+
queueHelp: "Running or resident sessions wait. Once generation stops, a resident writer still needs session unloading or a DSH restart. The queue then retries automatically; failed items can be retried or cancelled.",
|
|
378
378
|
queueRetryBtn: "Retry",
|
|
379
379
|
queueRetryAllBtn: "Retry all failed",
|
|
380
380
|
queueCancelBtn: "Cancel",
|
|
381
381
|
queueRetryDone: "✓ Migration retried",
|
|
382
382
|
queueRetryAllDone: "✓ Retried {n} failed migration(s) independently",
|
|
383
383
|
queueCancelled: "✓ Migration cancelled",
|
|
384
|
-
queuedMove: "Queued:
|
|
385
|
-
batchQueuedTail: "; {n} session(s) queued
|
|
384
|
+
queuedMove: "Queued: waiting for idle and writer release; a DSH restart may be required",
|
|
385
|
+
batchQueuedTail: "; {n} session(s) queued for safe migration",
|
|
386
386
|
previewStatus: "Preview status",
|
|
387
387
|
previewSource: "Source",
|
|
388
388
|
previewTarget: "Target path",
|
|
@@ -403,7 +403,7 @@ window.__ModuleLoader__.load({
|
|
|
403
403
|
forkHint: "Only the current session moves by default. Descendants are migrated independently and reported one by one.",
|
|
404
404
|
previewWarning: "Note",
|
|
405
405
|
previewSafe: "Safe to move",
|
|
406
|
-
previewQueued: "Will
|
|
406
|
+
previewQueued: "Will enter migration queue (restart may be required)",
|
|
407
407
|
previewBlocked: "Cannot move now",
|
|
408
408
|
previewWarningState: "Review before moving",
|
|
409
409
|
taskRetryBtn: "Retry failed",
|
|
@@ -752,6 +752,9 @@ window.__ModuleLoader__.load({
|
|
|
752
752
|
function renderPreview(plan) {
|
|
753
753
|
preview = plan;
|
|
754
754
|
if (!preview) return;
|
|
755
|
+
if (!batchCount && typeof preview.title === 'string' && preview.title.trim()) {
|
|
756
|
+
overlay.querySelector('.wsm-session').textContent = preview.title;
|
|
757
|
+
}
|
|
755
758
|
const statusLabels = {
|
|
756
759
|
safe: t("previewSafe"),
|
|
757
760
|
queued: t("previewQueued"),
|
|
@@ -901,8 +904,15 @@ window.__ModuleLoader__.load({
|
|
|
901
904
|
}
|
|
902
905
|
|
|
903
906
|
function rowTitle(row, sessionId) {
|
|
904
|
-
|
|
905
|
-
|
|
907
|
+
// Whole-row text also contains relative timestamps and status labels.
|
|
908
|
+
try {
|
|
909
|
+
const key = Object.getOwnPropertyNames(row ?? {}).find((name) => name.startsWith('__reactFiber$'));
|
|
910
|
+
for (let fiber = key ? row[key] : null; fiber; fiber = fiber.return) {
|
|
911
|
+
const node = fiber.memoizedProps?.node;
|
|
912
|
+
if (node && String(node.id) === String(sessionId) && typeof node.title === 'string' && node.title.trim()) return node.title.trim();
|
|
913
|
+
}
|
|
914
|
+
} catch { /* authoritative preview can supply the title */ }
|
|
915
|
+
return '';
|
|
906
916
|
}
|
|
907
917
|
|
|
908
918
|
function fmtBytes(n) {
|
|
@@ -986,7 +996,7 @@ window.__ModuleLoader__.load({
|
|
|
986
996
|
try {
|
|
987
997
|
const value = await call("mover.doctor");
|
|
988
998
|
setDoctor(value);
|
|
989
|
-
setNote(t("doctorSummary", value.summary));
|
|
999
|
+
setNote(`${t("doctorSummary", value.summary)}${value.pluginVersion ? ` · v${value.pluginVersion}` : ""}`);
|
|
990
1000
|
} catch (err) {
|
|
991
1001
|
setNote(t("failed", { msg: errMsg(err) }));
|
|
992
1002
|
} finally {
|
|
@@ -1862,12 +1872,57 @@ window.__ModuleLoader__.load({
|
|
|
1862
1872
|
};
|
|
1863
1873
|
let dragging = null; // {el, els, id, target} —— id/target 在 drop 阶段解析
|
|
1864
1874
|
let wsCache = null; // {items, at}
|
|
1875
|
+
// —— drag autoscroll:工作区列表长时,拖到视口外的目标工作区,指针靠近滚动容器边缘即持续滚动 ——
|
|
1876
|
+
let autoscrollRaf = null;
|
|
1877
|
+
let autoscrollState = null; // { scroller, clientX, clientY }
|
|
1878
|
+
/** 从 el 向上找最近的可纵向滚动的祖先(侧栏滚动区),不依赖具体 class,与行定位的 ARIA 策略一致。 */
|
|
1879
|
+
function findScrollableAncestor(el) {
|
|
1880
|
+
let node = el && el.parentElement;
|
|
1881
|
+
while (node) {
|
|
1882
|
+
try {
|
|
1883
|
+
const oy = getComputedStyle(node).overflowY;
|
|
1884
|
+
if ((oy === "auto" || oy === "scroll") && node.scrollHeight > node.clientHeight + 1) return node;
|
|
1885
|
+
} catch { /* getComputedStyle 不可用时跳过 */ }
|
|
1886
|
+
node = node.parentElement;
|
|
1887
|
+
}
|
|
1888
|
+
return null;
|
|
1889
|
+
}
|
|
1890
|
+
/** 停止 autoscroll raf 并清空状态。 */
|
|
1891
|
+
function stopAutoscroll() {
|
|
1892
|
+
if (autoscrollRaf !== null) { cancelAnimationFrame(autoscrollRaf); autoscrollRaf = null; }
|
|
1893
|
+
autoscrollState = null;
|
|
1894
|
+
}
|
|
1895
|
+
/** 每次 dragover 调用:更新指针 Y + 滚动容器;靠近边缘则 raf 持续滚动,离开边缘则停。 */
|
|
1896
|
+
function startAutoscroll(scroller, clientX, clientY) {
|
|
1897
|
+
if (!scroller || !Number.isFinite(clientX) || !Number.isFinite(clientY)) return stopAutoscroll();
|
|
1898
|
+
autoscrollState = { scroller, clientX, clientY };
|
|
1899
|
+
if (autoscrollRaf !== null) return; // 已在跑:tick 读最新 autoscrollState
|
|
1900
|
+
const tick = () => {
|
|
1901
|
+
autoscrollRaf = null;
|
|
1902
|
+
if (!dragging || !autoscrollState || !autoscrollState.scroller.isConnected) { stopAutoscroll(); return; }
|
|
1903
|
+
const sc = autoscrollState.scroller;
|
|
1904
|
+
const r = sc.getBoundingClientRect();
|
|
1905
|
+
const { clientX: x, clientY: y } = autoscrollState;
|
|
1906
|
+
if (x < r.left || x > r.right || y < r.top || y > r.bottom) return stopAutoscroll();
|
|
1907
|
+
const EDGE = Math.min(64, (r.bottom - r.top) / 3), STEP = 12;
|
|
1908
|
+
let delta = 0;
|
|
1909
|
+
if (y < r.top + EDGE) delta = -STEP;
|
|
1910
|
+
else if (y > r.bottom - EDGE) delta = STEP;
|
|
1911
|
+
if (!delta) return stopAutoscroll();
|
|
1912
|
+
const previous = sc.scrollTop;
|
|
1913
|
+
sc.scrollTop = Math.max(0, Math.min(sc.scrollHeight - sc.clientHeight, previous + delta));
|
|
1914
|
+
if (sc.scrollTop === previous) return stopAutoscroll();
|
|
1915
|
+
autoscrollRaf = requestAnimationFrame(tick);
|
|
1916
|
+
};
|
|
1917
|
+
autoscrollRaf = requestAnimationFrame(tick);
|
|
1918
|
+
}
|
|
1865
1919
|
|
|
1866
1920
|
// 统一登记本插件挂到 document / window 上的监听器与 observer。
|
|
1867
1921
|
// 必须成对释放:桌面版热重载会废弃旧 fiber 并重新装载客户端半,
|
|
1868
1922
|
// 不释放就会叠加第二份监听器——重复确认框、重复 RPC,
|
|
1869
1923
|
// 两份闭包还会互相擦掉对方的多选高亮。
|
|
1870
1924
|
const disposers = [];
|
|
1925
|
+
disposers.push(stopAutoscroll);
|
|
1871
1926
|
const listen = (type, fn, options) => {
|
|
1872
1927
|
document.addEventListener(type, fn, options);
|
|
1873
1928
|
disposers.push(() => document.removeEventListener(type, fn, options));
|
|
@@ -2195,6 +2250,8 @@ window.__ModuleLoader__.load({
|
|
|
2195
2250
|
listen("dragover", (e) => {
|
|
2196
2251
|
clearHints();
|
|
2197
2252
|
if (!dragging) return;
|
|
2253
|
+
// autoscroll:指针靠近侧栏滚动容器上下边缘时持续滚动,让远处目标工作区进入视口
|
|
2254
|
+
startAutoscroll(findScrollableAncestor(dragging.el), e.clientX, e.clientY);
|
|
2198
2255
|
const header = headerRow(e.target);
|
|
2199
2256
|
if (!header || isUngroupedHeader(header)) { dragging.target = null; return; }
|
|
2200
2257
|
e.preventDefault(); // 允许在此投放(没有它 drop 根本不会触发)
|
|
@@ -2212,6 +2269,7 @@ window.__ModuleLoader__.load({
|
|
|
2212
2269
|
listen("drop", async (e) => {
|
|
2213
2270
|
const current = dragging;
|
|
2214
2271
|
dragging = null;
|
|
2272
|
+
stopAutoscroll();
|
|
2215
2273
|
clearHints();
|
|
2216
2274
|
if (!current) return;
|
|
2217
2275
|
const header = headerRow(e.target);
|
|
@@ -2270,7 +2328,7 @@ window.__ModuleLoader__.load({
|
|
|
2270
2328
|
if (el === current.el) continue; // 拖起行用 dataTransfer 的权威 id
|
|
2271
2329
|
try {
|
|
2272
2330
|
const id = await resolveRowId(el);
|
|
2273
|
-
if (id) push(id, rowTitle(el,
|
|
2331
|
+
if (id) push(id, rowTitle(el, id));
|
|
2274
2332
|
else unresolvedCount += 1;
|
|
2275
2333
|
} catch { unresolvedCount += 1; }
|
|
2276
2334
|
}
|
|
@@ -2290,11 +2348,15 @@ window.__ModuleLoader__.load({
|
|
|
2290
2348
|
|
|
2291
2349
|
// ---- 单选:原有确认与 mover.move 路径 ----
|
|
2292
2350
|
if (sessions.length === 1) {
|
|
2293
|
-
const { sessionId
|
|
2351
|
+
const { sessionId } = sessions[0];
|
|
2352
|
+
let sessionTitle = sessions[0].sessionTitle;
|
|
2294
2353
|
let preview = null;
|
|
2295
2354
|
try {
|
|
2296
2355
|
const res = await rpcCall("mover.preview", { sessionId, targetWorkspaceId: workspace.workspaceId });
|
|
2297
|
-
if (res?.ok)
|
|
2356
|
+
if (res?.ok) {
|
|
2357
|
+
preview = res.value;
|
|
2358
|
+
if (typeof preview?.title === 'string' && preview.title.trim()) sessionTitle = preview.title;
|
|
2359
|
+
}
|
|
2298
2360
|
} catch (err) {
|
|
2299
2361
|
console.debug("[workspace-mover] preview unavailable:", err?.message ?? err);
|
|
2300
2362
|
}
|
|
@@ -2377,7 +2439,10 @@ window.__ModuleLoader__.load({
|
|
|
2377
2439
|
}
|
|
2378
2440
|
}, true);
|
|
2379
2441
|
|
|
2380
|
-
listen("dragend", () => { dragging = null; clearHints(); });
|
|
2442
|
+
listen("dragend", () => { dragging = null; stopAutoscroll(); clearHints(); });
|
|
2443
|
+
listen("dragleave", (e) => {
|
|
2444
|
+
if (autoscrollState && !autoscrollState.scroller.contains(e.relatedTarget)) stopAutoscroll();
|
|
2445
|
+
});
|
|
2381
2446
|
|
|
2382
2447
|
// 整组迁移 + 分组合并:入口在官方组标题的「⋯」菜单(注入「整组迁移…」项)。
|
|
2383
2448
|
// 不再拦截浏览器右键——官方菜单同样在右键/⋯按钮打开,拦截会与之冲突。
|