dsh-plugin-workspace-move-top 0.0.0-stage → 0.2.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/README.md CHANGED
@@ -1,3 +1,15 @@
1
- # Temporary Holding Version
1
+ # 工作区移到最上面
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 在 DSH 工作区 `…` 菜单中添加“移到最上面”。点击只调整一次顺序,随后仍可自由拖动;顺序由 Host 的 `workspaceRegistry.insertBefore` 保存。插件不保存运行时排序或收藏状态,启动与同步时不重新排序。
4
+
5
+ 仅支持 **DSH 0.2.1-rc.5**。
6
+
7
+ 安装:
8
+
9
+ ```sh
10
+ dsh plugin --profile desktop add dsh-plugin-workspace-move-top@0.2.0
11
+ ```
12
+
13
+ 当前 Host 没有工作区菜单扩展 slot。客户端依赖工作区行和菜单的 DOM 结构;宿主改变 DOM 后,菜单项可能不出现。Node 测试不能代替真实 Electron DOM 验证。
14
+
15
+ 验证:`npm run check`。
@@ -0,0 +1,133 @@
1
+ // One-shot workspace reorder. No favorites, pins, persistent state or row icons.
2
+ window.__ModuleLoader__.load({
3
+ id: 'dsh-plugin-workspace-move-top',
4
+ factory: function (require) {
5
+ var module = { exports: {} }
6
+ var exports = module.exports
7
+ var MENU_ITEM = 'data-wshub-move-top'
8
+ var API_PATH = 'api/plugins/wshub-workspace-move-top/move'
9
+
10
+ function apply(ctx) {
11
+ ctx.effect(function () {
12
+ var workspaceId = null
13
+ var busy = false
14
+ var observer = null
15
+ var timer = null
16
+ var buttons = new Set()
17
+
18
+ function rowId(target) {
19
+ var row = target && target.closest && target.closest('[data-row-key^="workspace:"]')
20
+ if (!row) return null
21
+ var key = row.getAttribute('data-row-key') || ''
22
+ return key.slice(10) || null
23
+ }
24
+
25
+ // Capture the row whose native workspace action button opened this menu.
26
+ function onPointerDown(event) {
27
+ if (!(event.target instanceof Element)) return
28
+ var button = event.target.closest('button[aria-label]')
29
+ if (event.target.closest('[role="menu"]')) return
30
+ if (!button || button.hasAttribute('aria-keyshortcuts')) {
31
+ workspaceId = null
32
+ return
33
+ }
34
+ var row = button.closest('[data-row-key^="workspace:"]')
35
+ workspaceId = row && row.querySelector('button[aria-label]') === button ? rowId(button) : null
36
+ }
37
+
38
+ async function moveToTop(button) {
39
+ var id = button.dataset.wshubWorkspaceId
40
+ if (!id || busy) return
41
+ busy = true
42
+ button.disabled = true
43
+ button.title = '正在调整顺序…'
44
+ try {
45
+ var response = await fetch(API_PATH, {
46
+ method: 'POST',
47
+ headers: { 'content-type': 'application/json' },
48
+ body: JSON.stringify({ workspaceId: id, action: 'move-to-top' }),
49
+ })
50
+ var result = await response.json().catch(function () { return null })
51
+ if (!response.ok || !result || result.ok !== true) throw new Error((result && result.message) || ('HTTP ' + response.status))
52
+ button.title = '已移到最上面'
53
+ // Closing the native menu removes the extension row with it.
54
+ var trigger = document.activeElement
55
+ if (trigger && trigger.closest && trigger.closest('[data-row-key^="workspace:"]')) trigger.click()
56
+ var event = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
57
+ document.dispatchEvent(event)
58
+ // DOM teardown is authoritative; the Host publishes the new order.
59
+ } catch (error) {
60
+ button.disabled = false
61
+ button.title = '调整失败:' + (error instanceof Error ? error.message : String(error))
62
+ button.setAttribute('aria-label', button.title)
63
+ } finally {
64
+ busy = false
65
+ }
66
+ }
67
+
68
+ function decorate() {
69
+ observer.disconnect()
70
+ try {
71
+ var menus = document.querySelectorAll('[role="menu"][data-menu-material="translucent"]')
72
+ for (var i = 0; i < menus.length; i++) {
73
+ var menu = menus[i]
74
+ if (!workspaceId || menu.querySelector('[' + MENU_ITEM + ']')) continue
75
+ var viewport = menu.querySelector('[role="presentation"]')
76
+ var template = viewport && viewport.querySelector('button[role="menuitem"]')
77
+ if (!template) continue
78
+ // Clone only native menu classes/row structure; do not copy its actions.
79
+ var wrapper = template.parentElement.cloneNode(false)
80
+ var button = template.cloneNode(false)
81
+ button.setAttribute(MENU_ITEM, '1')
82
+ button.dataset.wshubWorkspaceId = workspaceId
83
+ button.removeAttribute('class')
84
+ button.className = template.className
85
+ button.setAttribute('aria-label', '移到最上面')
86
+ button.title = '只调整一次顺序,之后仍可自由拖动'
87
+ var spans = template.querySelectorAll('span')
88
+ if (spans[0]) {
89
+ var icon = document.createElement('span')
90
+ icon.className = spans[0].className
91
+ icon.textContent = '↑'
92
+ button.appendChild(icon)
93
+ }
94
+ var label = document.createElement('span')
95
+ var labelTemplate = spans[1] || spans[0]
96
+ if (labelTemplate) label.className = labelTemplate.className
97
+ label.textContent = '移到最上面'
98
+ button.appendChild(label)
99
+ button.addEventListener('click', function (event) {
100
+ event.preventDefault()
101
+ event.stopPropagation()
102
+ void moveToTop(event.currentTarget)
103
+ })
104
+ wrapper.appendChild(button)
105
+ viewport.insertBefore(wrapper, viewport.firstChild)
106
+ buttons.add(button)
107
+ }
108
+ } finally {
109
+ observer.observe(document.documentElement, { childList: true, subtree: true })
110
+ }
111
+ }
112
+ function schedule() {
113
+ if (timer !== null) return
114
+ timer = setTimeout(function () { timer = null; decorate() }, 40)
115
+ }
116
+ observer = new MutationObserver(schedule)
117
+ observer.observe(document.documentElement, { childList: true, subtree: true })
118
+ document.addEventListener('pointerdown', onPointerDown, true)
119
+ document.addEventListener('keydown', onPointerDown, true)
120
+ return function () {
121
+ document.removeEventListener('pointerdown', onPointerDown, true)
122
+ document.removeEventListener('keydown', onPointerDown, true)
123
+ observer.disconnect()
124
+ if (timer !== null) clearTimeout(timer)
125
+ buttons.forEach(function (button) { button.remove() })
126
+ buttons.clear()
127
+ }
128
+ })
129
+ }
130
+ exports.apply = apply
131
+ return module.exports
132
+ },
133
+ })
@@ -0,0 +1,4 @@
1
+ - insert:
2
+ - id: wshub-workspace-move-top
3
+ name: dsh-plugin-workspace-move-top
4
+ # Stateless one-shot ordering; no favorites or saved plugin state.
@@ -0,0 +1,33 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "policy": "dsh-plugin-naming/v1",
4
+ "plugin": {
5
+ "namespace": "wshub",
6
+ "name": "workspace-move-top",
7
+ "coordinate": "wshub/workspace-move-top",
8
+ "packageName": "dsh-plugin-workspace-move-top"
9
+ },
10
+ "names": {
11
+ "pluginNames": [
12
+ "workspace-move-top"
13
+ ],
14
+ "loaderIds": [
15
+ "wshub-workspace-move-top"
16
+ ],
17
+ "services": [
18
+ "wshubWorkspaceMoveTop"
19
+ ],
20
+ "tools": [],
21
+ "commands": [],
22
+ "skills": [],
23
+ "skillProviders": [],
24
+ "events": [],
25
+ "settingsNamespaces": [],
26
+ "routes": [
27
+ {
28
+ "kind": "exact",
29
+ "path": "/api/plugins/wshub-workspace-move-top/move"
30
+ }
31
+ ]
32
+ }
33
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ export declare const name = "workspace-move-top";
3
+ export declare const inject: string[];
4
+ export { WorkspaceMoveTopService, SERVICE_NAME } from './service.js';
5
+ export { MOVE_ROUTE, handleMoveRequest } from './move-route.js';
6
+ /** Activate the stateless move-to-top service. No startup reordering: the
7
+ * host's durable registry order is left untouched until a client asks to move
8
+ * one workspace to the top.
9
+ * @param ctx injected plugin scope
10
+ */
11
+ export declare function apply(ctx: Context): Promise<void>;
package/lib/index.js ADDED
@@ -0,0 +1,15 @@
1
+ import { WorkspaceMoveTopService } from './service.js';
2
+ import { installMoveRoute } from './move-route.js';
3
+ export const name = 'workspace-move-top';
4
+ export const inject = ['workspaceRegistry'];
5
+ export { WorkspaceMoveTopService, SERVICE_NAME } from './service.js';
6
+ export { MOVE_ROUTE, handleMoveRequest } from './move-route.js';
7
+ /** Activate the stateless move-to-top service. No startup reordering: the
8
+ * host's durable registry order is left untouched until a client asks to move
9
+ * one workspace to the top.
10
+ * @param ctx injected plugin scope
11
+ */
12
+ export async function apply(ctx) {
13
+ const moveTop = new WorkspaceMoveTopService(ctx);
14
+ installMoveRoute(ctx, moveTop);
15
+ }
@@ -0,0 +1,17 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { WorkspaceMoveTopService } from './service.js';
3
+ /** Exact connection route the client calls for a one-shot move. */
4
+ export declare const MOVE_ROUTE = "/api/plugins/wshub-workspace-move-top/move";
5
+ export declare const MOVE_ACTION = "move-to-top";
6
+ /** Handle an authenticated request.
7
+ * @param moveTop serialized service
8
+ * @param request owner authenticated request
9
+ * @returns JSON response
10
+ */
11
+ export declare function handleMoveRequest(moveTop: WorkspaceMoveTopService, request: Request): Promise<Response>;
12
+ /** Register the optional connection route with an effect disposer.
13
+ * @param ctx plugin context
14
+ * @param moveTop serialized service
15
+ * @returns nothing
16
+ */
17
+ export declare function installMoveRoute(ctx: Context, moveTop: WorkspaceMoveTopService): void;
@@ -0,0 +1,40 @@
1
+ /** Exact connection route the client calls for a one-shot move. */
2
+ export const MOVE_ROUTE = '/api/plugins/wshub-workspace-move-top/move';
3
+ export const MOVE_ACTION = 'move-to-top';
4
+ /** Handle an authenticated request.
5
+ * @param moveTop serialized service
6
+ * @param request owner authenticated request
7
+ * @returns JSON response
8
+ */
9
+ export async function handleMoveRequest(moveTop, request) {
10
+ const headers = { 'cache-control': 'no-store' };
11
+ try {
12
+ if (request.method !== 'POST')
13
+ return Response.json({ ok: false, message: '不支持的请求方式' }, { status: 405, headers });
14
+ let body;
15
+ try {
16
+ body = await request.json();
17
+ }
18
+ catch {
19
+ return Response.json({ ok: false, message: '请求不是合法 JSON' }, { status: 400, headers });
20
+ }
21
+ if (!body || typeof body !== 'object' || Array.isArray(body) || !('workspaceId' in body) || typeof body.workspaceId !== 'string' || !body.workspaceId.trim() || !('action' in body) || body.action !== MOVE_ACTION) {
22
+ return Response.json({ ok: false, message: `需要工作区 ID 与 action: '${MOVE_ACTION}'` }, { status: 400, headers });
23
+ }
24
+ const result = await moveTop.moveToTop(body.workspaceId);
25
+ return Response.json(result, { status: result.ok ? 200 : 409, headers });
26
+ }
27
+ catch {
28
+ return Response.json({ ok: false, message: '移动未完成,请刷新后重试' }, { status: 500, headers });
29
+ }
30
+ }
31
+ /** Register the optional connection route with an effect disposer.
32
+ * @param ctx plugin context
33
+ * @param moveTop serialized service
34
+ * @returns nothing
35
+ */
36
+ export function installMoveRoute(ctx, moveTop) {
37
+ ctx.inject(['connection'], scope => {
38
+ scope.effect(() => scope.connection.fetch.register({ path: MOVE_ROUTE, methods: ['POST'], requestBody: 'buffered', fetch: request => handleMoveRequest(moveTop, request) }));
39
+ });
40
+ }
@@ -0,0 +1,47 @@
1
+ import { Service, type Context } from '@deepseek-ai/cordis';
2
+ export declare const SERVICE_NAME = "wshubWorkspaceMoveTop";
3
+ declare module '@deepseek-ai/cordis' {
4
+ interface Context {
5
+ wshubWorkspaceMoveTop: WorkspaceMoveTopService;
6
+ }
7
+ }
8
+ /** Registry projection needed for ordering and the route payload. */
9
+ export interface WorkspaceEntryView {
10
+ readonly id: string;
11
+ readonly path: string;
12
+ readonly title: string;
13
+ }
14
+ /** One successful move-to-top result. */
15
+ export interface MoveToTopResult {
16
+ readonly ok: true;
17
+ /** Whether an `insertBefore` actually moved the workspace. */
18
+ readonly moved: boolean;
19
+ /** The complete durable order after the operation (top first). */
20
+ readonly order: readonly string[];
21
+ readonly workspaces: readonly WorkspaceEntryView[];
22
+ }
23
+ /** Failure result for an empty registry or an unknown workspace. */
24
+ export interface MoveToTopError {
25
+ readonly ok: false;
26
+ readonly reason: 'none';
27
+ readonly message: string;
28
+ }
29
+ /** Stateless "move to top" service. Owns the registry ordering queue but no
30
+ * file or saved state: it never reorders on activation and keeps no ordering
31
+ * or favorites state. */
32
+ export declare class WorkspaceMoveTopService extends Service {
33
+ private queue;
34
+ constructor(ctx: Context);
35
+ private enqueue;
36
+ private list;
37
+ /** Move one workspace to the top of the durable display order, exactly once.
38
+ *
39
+ * Reads the real registry order; an already-first workspace is a no-op and an
40
+ * unknown workspace is rejected without touching the order. Otherwise it asks
41
+ * the host for `insertBefore(id, firstId)` — the same durable write the
42
+ * official sidebar drag uses — so the result survives restart and no plugin
43
+ * startup pass re-applies it.
44
+ * @param workspaceId exact registry ID
45
+ */
46
+ moveToTop(workspaceId: string): Promise<MoveToTopResult | MoveToTopError>;
47
+ }
package/lib/service.js ADDED
@@ -0,0 +1,48 @@
1
+ import { Service } from '@deepseek-ai/cordis';
2
+ export const SERVICE_NAME = 'wshubWorkspaceMoveTop';
3
+ function toEntryView(entry) {
4
+ return { id: String(entry.id), path: entry.path, title: entry.title };
5
+ }
6
+ /** Stateless "move to top" service. Owns the registry ordering queue but no
7
+ * file or saved state: it never reorders on activation and keeps no ordering
8
+ * or favorites state. */
9
+ export class WorkspaceMoveTopService extends Service {
10
+ queue = Promise.resolve();
11
+ constructor(ctx) {
12
+ super(ctx, SERVICE_NAME);
13
+ }
14
+ enqueue(action) {
15
+ const run = this.queue.then(action);
16
+ this.queue = run.then(() => undefined, () => undefined);
17
+ return run;
18
+ }
19
+ list() {
20
+ return this.ctx.workspaceRegistry.list().map(toEntryView);
21
+ }
22
+ /** Move one workspace to the top of the durable display order, exactly once.
23
+ *
24
+ * Reads the real registry order; an already-first workspace is a no-op and an
25
+ * unknown workspace is rejected without touching the order. Otherwise it asks
26
+ * the host for `insertBefore(id, firstId)` — the same durable write the
27
+ * official sidebar drag uses — so the result survives restart and no plugin
28
+ * startup pass re-applies it.
29
+ * @param workspaceId exact registry ID
30
+ */
31
+ moveToTop(workspaceId) {
32
+ return this.enqueue(async () => {
33
+ const registry = this.ctx.workspaceRegistry;
34
+ const order = registry.list();
35
+ const first = order[0];
36
+ if (first === undefined)
37
+ return { ok: false, reason: 'none', message: '工作区列表为空' };
38
+ if (first.id === workspaceId) {
39
+ return { ok: true, moved: false, order: order.map(entry => String(entry.id)), workspaces: order.map(toEntryView) };
40
+ }
41
+ if (!order.some(entry => entry.id === workspaceId)) {
42
+ return { ok: false, reason: 'none', message: '工作区已不存在,请刷新列表' };
43
+ }
44
+ const next = await registry.insertBefore(workspaceId, first.id);
45
+ return { ok: true, moved: true, order: next.map(id => String(id)), workspaces: this.list() };
46
+ });
47
+ }
48
+ }
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "dsh-plugin-workspace-move-top",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "DSH 工作区菜单:移到最上面。一次调整顺序,保留自由拖动,无收藏或固定置顶。",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./lib/index.js",
8
+ "types": "./lib/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./lib/index.d.ts",
12
+ "default": "./lib/index.js"
13
+ },
14
+ "./client": "./client/client.js",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "lib/",
19
+ "client/",
20
+ "cordis.patch.yml",
21
+ "README.md",
22
+ "dsh-plugin.naming.json"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/dongsheng123132/workspace-hub.git",
27
+ "directory": "plugins/dsh-workspace-move-top"
28
+ },
29
+ "dsh": {
30
+ "bundle": {
31
+ "patch": "./cordis.patch.yml"
32
+ },
33
+ "client": { "platform": "web" }
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.json",
37
+ "typecheck": "tsc -p tsconfig.json --noEmit",
38
+ "test": "node --test test/{client,composition}.test.mjs",
39
+ "check": "npm run typecheck && npm run build && npm test",
40
+ "prepack": "npm run build",
41
+ "pretest": "npm run build"
42
+ },
43
+ "peerDependencies": {
44
+ "@deepseek-ai/cordis": "4.0.5",
45
+ "@deepseek-ai/dsh-workspace": "0.2.1-rc.5",
46
+ "@deepseek-ai/dsh-client-connection": "0.2.1-rc.5"
47
+ },
48
+ "devDependencies": {
49
+ "@deepseek-ai/cordis": "4.0.5",
50
+ "@deepseek-ai/dsh-workspace": "0.2.1-rc.5",
51
+ "@deepseek-ai/dsh-client-connection": "0.2.1-rc.5",
52
+ "@types/node": "24.10.1",
53
+ "typescript": "5.9.3"
54
+ },
55
+ "engines": {
56
+ "node": ">=22"
57
+ },
58
+ "keywords": [
59
+ "dsh-plugin",
60
+ "workspace",
61
+ "move-top",
62
+ "ordering"
63
+ ],
64
+ "peerDependenciesMeta": {
65
+ "@deepseek-ai/dsh-client-connection": {
66
+ "optional": true
67
+ }
68
+ }
69
+ }