dsh-workspace-mover 2.2.0 → 2.3.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/package.json CHANGED
@@ -1,99 +1,102 @@
1
- {
2
- "name": "dsh-workspace-mover",
3
- "version": "2.2.0",
4
- "description": "在 DSH 侧边栏安全迁移会话:真迁移原始档案,支持失联会话恢复、移动历史、一键撤回和 agent 工具。",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "exports": {
8
- ".": "./lib/index.js",
9
- "./client": "./client/client.js",
10
- "./package.json": "./package.json"
11
- },
12
- "files": [
13
- "lib",
14
- "client",
15
- "test",
16
- "cordis.patch.yml",
17
- "README.md",
18
- "docs/ARCHITECTURE.md",
19
- "docs/media",
20
- "LICENSE"
21
- ], "scripts": {
22
- "test": "node --test test/core.test.mjs test/e2e-sandbox.test.mjs test/policy.test.mjs test/client-dom.test.mjs test/traffic-badge.test.mjs test/release-downloads.test.mjs",
23
- "check:syntax": "node --check lib/index.js && node --check client/client.js",
24
- "check:pack": "npm pack --dry-run",
25
- "check": "npm run check:syntax && npm test && npm run check:pack"
26
- },
27
- "keywords": [
28
- "deepseek-harness",
29
- "dsh",
30
- "dsh-plugin",
31
- "workspace",
32
- "session",
33
- "session-migration",
34
- "drag-and-drop",
35
- "agent-tools"
36
- ],
37
- "engines": {
38
- "node": ">=22",
39
- "dsh": ">=0.1.5-rc.1"
40
- },
41
- "//peerDependencies": "Deliberately `*`. These are OPTIONAL peers: the plugin declares them so storefronts can see the host packages it integrates with, but it fail-softs at runtime (mover.status capabilities + mover_doctor). npm's prerelease rule evaluates each `||` alternative as a set and only admits a prerelease when a comparator in that set shares the version's exact major.minor.patch tuple AND carries a prerelease tag -- so an upper bound like `<0.2.0-0` excludes EVERY 0.2.0 prerelease, which made the host report 2.2.0 as incompatible with 0.2.0-rc.2. Enumerating release lines would keep producing false incompatibilities on each new line; the real floor is engines.dsh, and capability flags report any actual runtime mismatch. Do NOT narrow these to version ranges.",
42
- "peerDependencies": {
43
- "@deepseek-ai/cordis": "*",
44
- "@deepseek-ai/dsh-session": "*",
45
- "@deepseek-ai/dsh-tools": "*",
46
- "@deepseek-ai/dsh-workspace": "*",
47
- "@deepseek-ai/schemastery": "*",
48
- "@deepseek-ai/dsh-client-connection": "*",
49
- "@deepseek-ai/dsh-client-locale": "*",
50
- "@deepseek-ai/dsh-client-ui-slots": "*"
51
- },
52
- "peerDependenciesMeta": {
53
- "@deepseek-ai/cordis": {
54
- "optional": true
55
- },
56
- "@deepseek-ai/dsh-session": {
57
- "optional": true
58
- },
59
- "@deepseek-ai/dsh-tools": {
60
- "optional": true
61
- },
62
- "@deepseek-ai/dsh-workspace": {
63
- "optional": true
64
- },
65
- "@deepseek-ai/schemastery": {
66
- "optional": true
67
- },
68
- "@deepseek-ai/dsh-client-connection": {
69
- "optional": true
70
- },
71
- "@deepseek-ai/dsh-client-locale": {
72
- "optional": true
73
- },
74
- "@deepseek-ai/dsh-client-ui-slots": {
75
- "optional": true
76
- }
77
- },
78
- "license": "MIT",
79
- "repository": {
80
- "type": "git",
81
- "url": "git+https://github.com/PianoPrince/dsh-workspace-mover.git"
82
- },
83
- "bugs": {
84
- "url": "https://github.com/PianoPrince/dsh-workspace-mover/issues"
85
- },
86
- "homepage": "https://github.com/PianoPrince/dsh-workspace-mover#readme",
87
- "dsh": {
88
- "bundle": {
89
- "patch": "./cordis.patch.yml"
90
- },
91
- "client": {
92
- "inject": [
93
- "@deepseek-ai/dsh-client-connection",
94
- "@deepseek-ai/dsh-client-ui-slots"
95
- ],
96
- "platform": "web"
97
- }
98
- }
99
- }
1
+ {
2
+ "name": "dsh-workspace-mover",
3
+ "version": "2.3.0",
4
+ "description": "在 DSH 侧边栏安全迁移会话:真迁移原始档案,支持失联会话恢复、移动历史、一键撤回和 agent 工具。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./client/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "lib",
14
+ "client",
15
+ "test",
16
+ "cordis.patch.yml",
17
+ "README.md",
18
+ "README_EN.md",
19
+ "CHANGELOG.md",
20
+ "docs/ARCHITECTURE.md",
21
+ "docs/compatibility",
22
+ "docs/media",
23
+ "LICENSE"
24
+ ], "scripts": {
25
+ "test": "node --test test/core.test.mjs test/e2e-sandbox.test.mjs test/policy.test.mjs test/client-dom.test.mjs test/traffic-badge.test.mjs test/release-downloads.test.mjs",
26
+ "check:syntax": "node --check lib/index.js && node --check client/client.js",
27
+ "check:pack": "npm pack --dry-run",
28
+ "check": "npm run check:syntax && npm test && npm run check:pack"
29
+ },
30
+ "keywords": [
31
+ "deepseek-harness",
32
+ "dsh",
33
+ "dsh-plugin",
34
+ "workspace",
35
+ "session",
36
+ "session-migration",
37
+ "drag-and-drop",
38
+ "agent-tools"
39
+ ],
40
+ "engines": {
41
+ "node": ">=22",
42
+ "dsh": ">=0.1.5-rc.1"
43
+ },
44
+ "//peerDependencies": "Deliberately `*`. These are OPTIONAL peers: the plugin declares them so storefronts can see the host packages it integrates with, but it fail-softs at runtime (mover.status capabilities + mover_doctor). npm's prerelease rule evaluates each `||` alternative as a set and only admits a prerelease when a comparator in that set shares the version's exact major.minor.patch tuple AND carries a prerelease tag -- so an upper bound like `<0.2.0-0` excludes EVERY 0.2.0 prerelease, which made the host report 2.2.0 as incompatible with 0.2.0-rc.2. Enumerating release lines would keep producing false incompatibilities on each new line; the real floor is engines.dsh, and capability flags report any actual runtime mismatch. Do NOT narrow these to version ranges.",
45
+ "peerDependencies": {
46
+ "@deepseek-ai/cordis": "*",
47
+ "@deepseek-ai/dsh-session": "*",
48
+ "@deepseek-ai/dsh-tools": "*",
49
+ "@deepseek-ai/dsh-workspace": "*",
50
+ "@deepseek-ai/schemastery": "*",
51
+ "@deepseek-ai/dsh-client-connection": "*",
52
+ "@deepseek-ai/dsh-client-locale": "*",
53
+ "@deepseek-ai/dsh-client-ui-slots": "*"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "@deepseek-ai/cordis": {
57
+ "optional": true
58
+ },
59
+ "@deepseek-ai/dsh-session": {
60
+ "optional": true
61
+ },
62
+ "@deepseek-ai/dsh-tools": {
63
+ "optional": true
64
+ },
65
+ "@deepseek-ai/dsh-workspace": {
66
+ "optional": true
67
+ },
68
+ "@deepseek-ai/schemastery": {
69
+ "optional": true
70
+ },
71
+ "@deepseek-ai/dsh-client-connection": {
72
+ "optional": true
73
+ },
74
+ "@deepseek-ai/dsh-client-locale": {
75
+ "optional": true
76
+ },
77
+ "@deepseek-ai/dsh-client-ui-slots": {
78
+ "optional": true
79
+ }
80
+ },
81
+ "license": "MIT",
82
+ "repository": {
83
+ "type": "git",
84
+ "url": "git+https://github.com/PianoPrince/dsh-workspace-mover.git"
85
+ },
86
+ "bugs": {
87
+ "url": "https://github.com/PianoPrince/dsh-workspace-mover/issues"
88
+ },
89
+ "homepage": "https://github.com/PianoPrince/dsh-workspace-mover#readme",
90
+ "dsh": {
91
+ "bundle": {
92
+ "patch": "./cordis.patch.yml"
93
+ },
94
+ "client": {
95
+ "inject": [
96
+ "@deepseek-ai/dsh-client-connection",
97
+ "@deepseek-ai/dsh-client-ui-slots"
98
+ ],
99
+ "platform": "web"
100
+ }
101
+ }
102
+ }
@@ -562,6 +562,135 @@ test('工作区解析必须自证:标题与路径都不匹配时不得按 DOM
562
562
  assert.equal(calls.length, 0, '标题与路径都不匹配时必须中止,不得按序号回退');
563
563
  });
564
564
 
565
+ test('工作区身份优先于标题:移入后仍可拖回原工作区', async () => {
566
+ resetDom();
567
+ mount();
568
+ const row = sessionRow('Alpha discussion', 'session-aaa');
569
+ const beta = header('proj-beta');
570
+ const alpha = header('proj-beta'); // Simulate stale visible text after a sidebar update.
571
+ Object.defineProperty(alpha, '__reactFiber$t', { value: {
572
+ memoizedProps: { group: { workspaceId: 'wid-a' } }, return: null
573
+ } });
574
+ fixture({ rows: [row] });
575
+ attach(beta, alpha);
576
+ await dragDrop(row, beta, 'session-aaa');
577
+ await dragDrop(row, alpha, 'session-aaa');
578
+ assert.deepEqual(moveCalls().map((call) => call.payload.targetWorkspaceId), ['wid-b', 'wid-a']);
579
+ });
580
+
581
+ test('工作区 data-row-key 优先于包含其他工作区名称的文字', async () => {
582
+ resetDom();
583
+ mount();
584
+ const row = sessionRow('Alpha discussion', 'session-aaa');
585
+ const beta = header('proj-alpha proj-beta');
586
+ beta.setAttribute('data-row-key', 'workspace:wid-b');
587
+ Object.defineProperty(beta, '__reactFiber$t', { value: {
588
+ memoizedProps: { group: { workspaceId: 'wid-a' } }, return: null
589
+ } }); // Rendered DOM identity must win over stale React props.
590
+ fixture({ rows: [row] });
591
+ attach(beta);
592
+ await dragDrop(row, beta, 'session-aaa');
593
+ assert.equal(moveCalls()[0]?.payload.targetWorkspaceId, 'wid-b');
594
+ });
595
+
596
+ test('嵌套会话标题不会被用作目标工作区名称', async () => {
597
+ resetDom();
598
+ mount();
599
+ const row = sessionRow('Alpha discussion', 'session-aaa');
600
+ const beta = header('proj-beta');
601
+ beta.appendChild(sessionRow('proj-alpha', 'session-other'));
602
+ fixture({ rows: [row] });
603
+ attach(beta);
604
+ await dragDrop(row, beta, 'session-aaa');
605
+ assert.equal(moveCalls()[0]?.payload.targetWorkspaceId, 'wid-b');
606
+ });
607
+
608
+ test('工作区文字匹配有歧义时停止迁移', async () => {
609
+ resetDom();
610
+ mount();
611
+ const row = sessionRow('Alpha discussion', 'session-aaa');
612
+ const ambiguous = header('proj-alpha proj-beta');
613
+ fixture({ rows: [row] });
614
+ attach(ambiguous);
615
+ await dragDrop(row, ambiguous, 'session-aaa');
616
+ assert.equal(moveCalls().length, 0);
617
+ });
618
+
619
+ test('落在标题外时不沿用上次悬停的工作区', async () => {
620
+ resetDom();
621
+ mount();
622
+ const row = sessionRow('Alpha discussion', 'session-aaa');
623
+ const beta = header('proj-beta');
624
+ fixture({ rows: [row] });
625
+ attach(beta);
626
+ const dt = { getData: () => 'session-aaa' };
627
+ dispatch({ type: 'dragstart', target: row, dataTransfer: dt });
628
+ dispatch({ type: 'dragover', target: beta, preventDefault() {} });
629
+ await new Promise((r) => setTimeout(r, 0));
630
+ dispatch({ type: 'drop', target: doc._body, dataTransfer: dt, preventDefault() {}, stopImmediatePropagation() {} });
631
+ await new Promise((r) => setTimeout(r, 0));
632
+ assert.equal(doc._body.querySelector('.wsm-overlay'), null);
633
+ assert.equal(moveCalls().length, 0);
634
+ });
635
+
636
+ test('同工作区预览阻断后可显式选择原工作区并重新预览再移回', async () => {
637
+ resetDom();
638
+ mount();
639
+ const row = sessionRow('Alpha discussion', 'session-aaa');
640
+ const beta = header('proj-beta');
641
+ const { ws } = fixture({ rows: [row] });
642
+ attach(beta);
643
+ setResponder((endpoint, payload) => {
644
+ if (endpoint === 'mover.workspaces') return { ok: true, value: { items: ws } };
645
+ if (endpoint === 'mover.preview') return { ok: true, value: {
646
+ status: payload.targetWorkspaceId === 'wid-b' ? 'blocked' : 'safe',
647
+ source: { cwd: 'C:/proj-beta' },
648
+ target: { path: ws.find((item) => item.workspaceId === payload.targetWorkspaceId).path },
649
+ warnings: [], estimatedBackupBytes: 123
650
+ } };
651
+ return { ok: true, value: {} };
652
+ });
653
+ await dragDrop(row, beta, 'session-aaa', { confirm: false });
654
+ const select = doc._body.querySelector('.wsm-target-select');
655
+ const ok = doc._body.querySelector('.wsm-ok');
656
+ assert.equal(ok.disabled, true);
657
+ select.value = 'wid-a';
658
+ dispatch({ type: 'change', target: select });
659
+ assert.equal(ok.disabled, true, 'Must wait for the new preview');
660
+ await new Promise((r) => setTimeout(r, 0));
661
+ assert.equal(ok.disabled, false);
662
+ assert.match(doc._body.querySelector('.wsm-preview').textContent, /目标路径: C:\/proj-alpha/);
663
+ dispatch({ type: 'click', target: ok });
664
+ await new Promise((r) => setTimeout(r, 0));
665
+ assert.equal(moveCalls()[0]?.payload.targetWorkspaceId, 'wid-a');
666
+ });
667
+
668
+ test('切换目标后预览失败时禁止按旧预览执行', async () => {
669
+ resetDom();
670
+ mount();
671
+ const row = sessionRow('Alpha discussion', 'session-aaa');
672
+ const beta = header('proj-beta');
673
+ const { ws } = fixture({ rows: [row] });
674
+ attach(beta);
675
+ setResponder((endpoint, payload) => {
676
+ if (endpoint === 'mover.workspaces') return { ok: true, value: { items: ws } };
677
+ if (endpoint === 'mover.preview') return payload.targetWorkspaceId === 'wid-a'
678
+ ? { ok: false, error: { message: 'preview failed' } }
679
+ : { ok: true, value: { status: 'safe', source: { cwd: 'C:/proj-alpha' } } };
680
+ return { ok: true, value: {} };
681
+ });
682
+ await dragDrop(row, beta, 'session-aaa', { confirm: false });
683
+ const select = doc._body.querySelector('.wsm-target-select');
684
+ select.value = 'wid-a';
685
+ dispatch({ type: 'change', target: select });
686
+ await new Promise((r) => setTimeout(r, 0));
687
+ const ok = doc._body.querySelector('.wsm-ok');
688
+ assert.equal(ok.disabled, true);
689
+ dispatch({ type: 'click', target: ok });
690
+ assert.equal(moveCalls().length, 0);
691
+ dispatch({ type: 'click', target: doc._body.querySelector('.wsm-cancel') });
692
+ });
693
+
565
694
  test('弹窗 Escape 关闭并释放 keydown 监听器', async () => {
566
695
  resetDom();
567
696
  mount();