dsh-plugin-workspace-move-top 0.0.0-stage → 0.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Workspace Hub contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,29 @@
1
- # Temporary Holding Version
1
+ # Move to Top
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
+ [中文说明](README.zh-CN.md)
4
+
5
+ Add **Move to top** to a DSH workspace's `…` menu. One click performs one reorder through the host's official `workspaceRegistry.insertBefore` API, just like a long manual drag.
6
+
7
+ - No stars, favorites, pinned categories, or automatic reordering.
8
+ - Move another workspace later and the previous one naturally moves down.
9
+ - Manual dragging remains yours; activation and synchronization never restore an old order.
10
+ - Works independently of Workspace Hub.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ dsh plugin --profile desktop add dsh-plugin-workspace-move-top@0.2.1
16
+ ```
17
+
18
+ ## Compatibility and limits
19
+
20
+ Tested with **DSH 0.2.1-rc.5 on macOS**. This release targets that exact host version. Menu copy follows the browser language: Chinese or English.
21
+
22
+ The host has no workspace-menu extension slot, so the client uses the current workspace-row and menu DOM. If a future host changes that structure, the action may disappear. It must not reorder workspaces on its own. Windows and non-empty session-menu browser behavior have not been verified.
23
+
24
+ ## Verify
25
+
26
+ ```sh
27
+ npm ci
28
+ npm run check
29
+ ```
@@ -0,0 +1,29 @@
1
+ # 移到最上面
2
+
3
+ [English](README.md)
4
+
5
+ 在 DSH 工作区 `…` 菜单中增加“移到最上面”。点击一次,通过官方 `workspaceRegistry.insertBefore` 接口完成一次排序,相当于替你完成一次长距离拖动。
6
+
7
+ - 没有星标、收藏区或自动归位。
8
+ - 之后把另一个项目移到第一,原来的自然退到第二。
9
+ - 仍可自由拖动;启动与同步时不恢复旧顺序。
10
+ - 不依赖工作区同步插件,可单独安装。
11
+
12
+ ## 安装
13
+
14
+ ```sh
15
+ dsh plugin --profile desktop add dsh-plugin-workspace-move-top@0.2.1
16
+ ```
17
+
18
+ ## 兼容性与限制
19
+
20
+ 已在 **macOS + DSH 0.2.1-rc.5** 验证,本版本精确对应该宿主。菜单根据浏览器语言显示中文或英文。
21
+
22
+ 宿主没有工作区菜单扩展位,因此客户端使用当前工作区行和菜单 DOM。宿主改变该结构后,菜单项可能消失;插件不会因此自行排序。Windows 真机与非空会话菜单的浏览器行为尚未验证。
23
+
24
+ ## 验证
25
+
26
+ ```sh
27
+ npm ci
28
+ npm run check
29
+ ```
@@ -0,0 +1,139 @@
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
+ // Menu copy follows the host UI language (navigator.language). Backend error
10
+ // messages are shown verbatim; only our own prefix is localized.
11
+ var LANGUAGE = (typeof navigator !== 'undefined' && navigator && navigator.language) || ''
12
+ var LABELS = /^zh/i.test(LANGUAGE)
13
+ ? { menu: '移到最上面', hint: '只调整一次顺序,之后仍可自由拖动', busy: '正在调整顺序…', done: '已移到最上面', failed: '调整失败:' }
14
+ : { menu: 'Move to top', hint: 'Moves this workspace once; you can still drag afterwards.', busy: 'Moving to top…', done: 'Moved to top', failed: 'Move failed: ' }
15
+
16
+ function apply(ctx) {
17
+ ctx.effect(function () {
18
+ var workspaceId = null
19
+ var busy = false
20
+ var observer = null
21
+ var timer = null
22
+ var buttons = new Set()
23
+
24
+ function rowId(target) {
25
+ var row = target && target.closest && target.closest('[data-row-key^="workspace:"]')
26
+ if (!row) return null
27
+ var key = row.getAttribute('data-row-key') || ''
28
+ return key.slice(10) || null
29
+ }
30
+
31
+ // Capture the row whose native workspace action button opened this menu.
32
+ function onPointerDown(event) {
33
+ if (!(event.target instanceof Element)) return
34
+ var button = event.target.closest('button[aria-label]')
35
+ if (event.target.closest('[role="menu"]')) return
36
+ if (!button || button.hasAttribute('aria-keyshortcuts')) {
37
+ workspaceId = null
38
+ return
39
+ }
40
+ var row = button.closest('[data-row-key^="workspace:"]')
41
+ workspaceId = row && row.querySelector('button[aria-label]') === button ? rowId(button) : null
42
+ }
43
+
44
+ async function moveToTop(button) {
45
+ var id = button.dataset.wshubWorkspaceId
46
+ if (!id || busy) return
47
+ busy = true
48
+ button.disabled = true
49
+ button.title = LABELS.busy
50
+ try {
51
+ var response = await fetch(API_PATH, {
52
+ method: 'POST',
53
+ headers: { 'content-type': 'application/json' },
54
+ body: JSON.stringify({ workspaceId: id, action: 'move-to-top' }),
55
+ })
56
+ var result = await response.json().catch(function () { return null })
57
+ if (!response.ok || !result || result.ok !== true) throw new Error((result && result.message) || ('HTTP ' + response.status))
58
+ button.title = LABELS.done
59
+ // Closing the native menu removes the extension row with it.
60
+ var trigger = document.activeElement
61
+ if (trigger && trigger.closest && trigger.closest('[data-row-key^="workspace:"]')) trigger.click()
62
+ var event = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
63
+ document.dispatchEvent(event)
64
+ // DOM teardown is authoritative; the Host publishes the new order.
65
+ } catch (error) {
66
+ button.disabled = false
67
+ button.title = LABELS.failed + (error instanceof Error ? error.message : String(error))
68
+ button.setAttribute('aria-label', button.title)
69
+ } finally {
70
+ busy = false
71
+ }
72
+ }
73
+
74
+ function decorate() {
75
+ observer.disconnect()
76
+ try {
77
+ var menus = document.querySelectorAll('[role="menu"][data-menu-material="translucent"]')
78
+ for (var i = 0; i < menus.length; i++) {
79
+ var menu = menus[i]
80
+ if (!workspaceId || menu.querySelector('[' + MENU_ITEM + ']')) continue
81
+ var viewport = menu.querySelector('[role="presentation"]')
82
+ var template = viewport && viewport.querySelector('button[role="menuitem"]')
83
+ if (!template) continue
84
+ // Clone only native menu classes/row structure; do not copy its actions.
85
+ var wrapper = template.parentElement.cloneNode(false)
86
+ var button = template.cloneNode(false)
87
+ button.setAttribute(MENU_ITEM, '1')
88
+ button.dataset.wshubWorkspaceId = workspaceId
89
+ button.removeAttribute('class')
90
+ button.className = template.className
91
+ button.setAttribute('aria-label', LABELS.menu)
92
+ button.title = LABELS.hint
93
+ var spans = template.querySelectorAll('span')
94
+ if (spans[0]) {
95
+ var icon = document.createElement('span')
96
+ icon.className = spans[0].className
97
+ icon.textContent = '↑'
98
+ button.appendChild(icon)
99
+ }
100
+ var label = document.createElement('span')
101
+ var labelTemplate = spans[1] || spans[0]
102
+ if (labelTemplate) label.className = labelTemplate.className
103
+ label.textContent = LABELS.menu
104
+ button.appendChild(label)
105
+ button.addEventListener('click', function (event) {
106
+ event.preventDefault()
107
+ event.stopPropagation()
108
+ void moveToTop(event.currentTarget)
109
+ })
110
+ wrapper.appendChild(button)
111
+ viewport.insertBefore(wrapper, viewport.firstChild)
112
+ buttons.add(button)
113
+ }
114
+ } finally {
115
+ observer.observe(document.documentElement, { childList: true, subtree: true })
116
+ }
117
+ }
118
+ function schedule() {
119
+ if (timer !== null) return
120
+ timer = setTimeout(function () { timer = null; decorate() }, 40)
121
+ }
122
+ observer = new MutationObserver(schedule)
123
+ observer.observe(document.documentElement, { childList: true, subtree: true })
124
+ document.addEventListener('pointerdown', onPointerDown, true)
125
+ document.addEventListener('keydown', onPointerDown, true)
126
+ return function () {
127
+ document.removeEventListener('pointerdown', onPointerDown, true)
128
+ document.removeEventListener('keydown', onPointerDown, true)
129
+ observer.disconnect()
130
+ if (timer !== null) clearTimeout(timer)
131
+ buttons.forEach(function (button) { button.remove() })
132
+ buttons.clear()
133
+ }
134
+ })
135
+ }
136
+ exports.apply = apply
137
+ return module.exports
138
+ },
139
+ })
@@ -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": "dongsheng123132",
6
+ "name": "workspace-move-top",
7
+ "coordinate": "dongsheng123132/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,72 @@
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.1",
4
+ "description": "Move a workspace to the top once from its menu, without favorites or automatic reordering.",
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
+ "README.zh-CN.md",
23
+ "dsh-plugin.naming.json"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/dongsheng123132/dsh-workspace-plugins.git",
28
+ "directory": "plugins/dsh-workspace-move-top"
29
+ },
30
+ "dsh": {
31
+ "bundle": {
32
+ "patch": "./cordis.patch.yml"
33
+ },
34
+ "client": {
35
+ "platform": "web"
36
+ }
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -p tsconfig.json",
40
+ "typecheck": "tsc -p tsconfig.json --noEmit",
41
+ "test": "node --test test/{client,composition}.test.mjs",
42
+ "check": "npm run typecheck && npm run build && npm test",
43
+ "prepack": "npm run build",
44
+ "pretest": "npm run build"
45
+ },
46
+ "peerDependencies": {
47
+ "@deepseek-ai/cordis": "4.0.5",
48
+ "@deepseek-ai/dsh-workspace": "0.2.1-rc.5",
49
+ "@deepseek-ai/dsh-client-connection": "0.2.1-rc.5"
50
+ },
51
+ "devDependencies": {
52
+ "@deepseek-ai/cordis": "4.0.5",
53
+ "@deepseek-ai/dsh-workspace": "0.2.1-rc.5",
54
+ "@deepseek-ai/dsh-client-connection": "0.2.1-rc.5",
55
+ "@types/node": "24.10.1",
56
+ "typescript": "5.9.3"
57
+ },
58
+ "engines": {
59
+ "node": ">=22"
60
+ },
61
+ "keywords": [
62
+ "dsh-plugin",
63
+ "workspace",
64
+ "move-top",
65
+ "ordering"
66
+ ],
67
+ "peerDependenciesMeta": {
68
+ "@deepseek-ai/dsh-client-connection": {
69
+ "optional": true
70
+ }
71
+ }
72
+ }