dsh-workspace-toolkit 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gege9527
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 ADDED
@@ -0,0 +1,115 @@
1
+ # dsh-workspace-toolkit
2
+
3
+ [![npm version](https://img.shields.io/npm/v/dsh-workspace-toolkit.svg)](https://www.npmjs.com/package/dsh-workspace-toolkit)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+ [![DSH Plugin](https://img.shields.io/badge/DSH-plugin-orange.svg)](https://github.com/deepseek-ai/dsh)
6
+
7
+ > DSH workspace enhancement toolkit: add "Open in File Explorer" and "Batch Archive Sessions" actions to every workspace row in the DSH sidebar.
8
+ >
9
+ > DSH 工作区增强工具包:在资源管理器侧边栏的每个工作区行注入“在文件资源管理器中打开”和“批量归档会话”快捷操作。
10
+
11
+ ## Features
12
+
13
+ - **在文件资源管理器中打开 / Open in File Explorer**
14
+ - 出现在每个 workspace 行右侧的“⋮”菜单中。
15
+ - 调用 DSH 原生 API `session.openWorkspacePath(path)`,由后端跨平台打开目录:
16
+ - Windows → 文件资源管理器
17
+ - macOS → Finder
18
+ - Linux → 默认文件管理器(xdg-open)
19
+ - 若路径尚未获取,会提示先将鼠标悬停到该工作区上以加载 hover card。
20
+
21
+ - **批量归档会话… / Batch Archive Sessions…**
22
+ - 从工作区菜单打开会话列表面板。
23
+ - 列出该工作区下所有非空白、未归档的会话(运行中的会话半透明显示,由用户自行决定是否归档)。
24
+ - 支持全选 / 取消全选、显示更新时间、顺序调用 `uiWorkspace.archiveSession(sessionId)`,单个失败即暂停并提醒,避免并发覆盖 archive set。
25
+
26
+ ## Requirements
27
+
28
+ - [Node.js](https://nodejs.org/) >= 18
29
+ - DSH CLI / Web Client(本插件针对 DSH 工作区侧边栏的 DOM 结构实现)
30
+ - Peer dependency: `@deepseek-ai/dsh-tools >= 0.0.1-rc.1`
31
+
32
+ ## Installation
33
+
34
+ ### As a DSH plugin(推荐 / Recommended)
35
+
36
+ ```bash
37
+ # 替换 <your-profile> 为实际 profile 名
38
+ dsh plugin --profile <your-profile> add dsh-workspace-toolkit
39
+ ```
40
+
41
+ 添加后刷新或重启 DSH Web 客户端即可生效。
42
+
43
+ ### From npm / 从 npm 安装
44
+
45
+ ```bash
46
+ npm install dsh-workspace-toolkit
47
+ ```
48
+
49
+ 在 DSH profile 中按本地插件引用(示例):
50
+
51
+ ```yaml
52
+ # your-profile.yml
53
+ plugins:
54
+ - id: workspace-toolkit
55
+ package: ./node_modules/dsh-workspace-toolkit
56
+ ```
57
+
58
+ > 实际路径和配置方式请遵循你使用的 DSH 版本文档。
59
+
60
+ ## Usage
61
+
62
+ 1. 打开 DSH Web 客户端左侧的工作区(Workspace)侧边栏。
63
+ 2. 将鼠标悬停在任一工作区行上,等待 hover card 出现,插件会自动记录该工作区路径。
64
+ 3. 点击工作区行右侧的 `⋮`(Workspace actions)菜单。
65
+ 4. 选择:
66
+ - **在文件资源管理器中打开 / Open in File Explorer** — 直接打开目录。
67
+ - **批量归档会话… / Batch Archive Sessions…** — 勾选需要归档的会话,点击“归档所选”。
68
+
69
+ ## File Structure
70
+
71
+ ```text
72
+ index.js # Host half entry point. No server-side state is required.
73
+ client.js # Browser half. Observes workspace rows/menus, injects menu items, renders the batch-archive dialog.
74
+ cordis.patch.yml # Cordis bundle patch used by DSH to load the browser plugin.
75
+ ```
76
+
77
+ ## How It Works
78
+
79
+ - **DOM observation**: 监听 `document.body` 下的 hover card 与 `[role="menu"]`,识别 workspace 菜单后注入两个自定义 `menuitem`。
80
+ - **Workspace path**: 从 DSH hover card 的 `aria-label` 或路径文本提取实际目录路径,并缓存到对应 workspace 行上。
81
+ - **Batch archive**: 打开弹窗时读取 `ctx.workspaces.list` 与 `ctx.sessions.list` 的快照,筛选出可归档会话,按顺序逐个归档。
82
+ - **Internationalization**: 文案根据浏览器 `navigator.language` 在中文与英文间自动切换;暂时无法跟随 DSH 内部语言热切换。
83
+
84
+ ## Limitations
85
+
86
+ - 本插件不修改 DSH core,依赖 workspace 行与菜单的 DOM 结构;若 DSH 更新后 DOM 变化,插件可能需要同步更新。
87
+ - 批量归档弹窗基于 `ctx.workspaces.list` 和 `ctx.sessions.list` 渲染,不直接操作会话行 DOM,因此相对稳定。
88
+ - 菜单文案基于 `navigator.language` 简单判断,无法跟随 DSH 内部语言切换。
89
+ - 必须先悬停工作区使 hover card 出现,否则“在资源管理器中打开”可能无法获取路径。
90
+ - 批量归档使用顺序调用以避免并发覆盖;单个失败会暂停并弹窗提示。
91
+
92
+ ## Development
93
+
94
+ ```bash
95
+ # 克隆仓库后可直接在本地引用测试
96
+ git clone https://github.com/<your-username>/dsh-workspace-toolkit.git
97
+ cd dsh-workspace-toolkit
98
+ dsh plugin --profile <your-profile> add .
99
+ ```
100
+
101
+ 本项目不含构建步骤,源码即分发文件。修改 `client.js` 后刷新 DSH Web 客户端即可看到效果。
102
+
103
+ ## Publish Checklist
104
+
105
+ 在发布到 npm / 推送 GitHub 前,请确认:
106
+
107
+ - [ ] 修改 `package.json` 中的 `repository.url` 和 `bugs.url` 为你自己的仓库地址。
108
+ - [ ] 修改 `package.json` 中的 `author` 字段(如需要)。
109
+ - [ ] 运行 `npm version <patch|minor|major>` 提升版本号。
110
+ - [ ] 运行 `npm publish` 发布到 npm(需先登录 `npm login`)。
111
+ - [ ] 在 GitHub 创建同名仓库并推送代码。
112
+
113
+ ## License
114
+
115
+ [MIT](LICENSE)
package/client.js ADDED
@@ -0,0 +1,606 @@
1
+ // Browser half of dsh-workspace-toolkit.
2
+ // Workspace utility toolkit: open workspace path in file explorer, batch archive
3
+ // sessions, and future file/session operations.
4
+ // Injects items into each workspace row's ellipsis menu using DOM observation.
5
+
6
+ window.__ModuleLoader__.load({
7
+ id: 'dsh-workspace-toolkit',
8
+ factory: function (require) {
9
+ var module = { exports: {} }
10
+ var exports = module.exports
11
+
12
+ var NS = 'dsh-workspace-toolkit'
13
+
14
+ // ---- Lightweight locale (DSH locale switching is not reachable from an external plugin). ----
15
+ function isZh() {
16
+ var lang = (typeof navigator !== 'undefined' && navigator.language) || 'en'
17
+ return lang.indexOf('zh') === 0
18
+ }
19
+ var LABELS = {
20
+ openInExplorer: isZh() ? '在文件资源管理器中打开' : 'Open in File Explorer',
21
+ pathNotFound: isZh() ? '未找到工作区路径,请先将鼠标悬停在该工作区上。' : 'Workspace path not found. Please hover over the workspace first.',
22
+ openFailed: isZh() ? '打开失败:' : 'Failed to open: ',
23
+ batchArchive: isZh() ? '批量归档会话…' : 'Batch Archive Sessions…',
24
+ batchArchiveTitle: isZh() ? '批量归档会话' : 'Batch Archive Sessions',
25
+ selectAll: isZh() ? '全选' : 'Select all',
26
+ archiveSelected: isZh() ? '归档所选' : 'Archive selected',
27
+ cancel: isZh() ? '取消' : 'Cancel',
28
+ close: isZh() ? '关闭' : 'Close',
29
+ noSessions: isZh() ? '该工作区下没有可归档的会话。' : 'No archivable sessions in this workspace.',
30
+ loading: isZh() ? '加载中…' : 'Loading…',
31
+ archiveFailed: isZh() ? '归档失败:' : 'Archive failed: ',
32
+ archivedCount: isZh() ? '已归档 {n} 个会话' : 'Archived {n} sessions',
33
+ selectedCount: isZh() ? '已选择 {n} 个' : '{n} selected',
34
+ workspaceNotFound: isZh() ? '未找到该工作区,请重试。' : 'Workspace not found. Please try again.'
35
+ }
36
+
37
+ // ---- Workspace row / path tracking ----
38
+ var lastMouseX = 0
39
+ var lastMouseY = 0
40
+ var pendingWorkspace = null // { workspaceId, path, title, expires }
41
+
42
+ function isWorkspaceMenuButton(btn) {
43
+ var label = btn.getAttribute('aria-label') || ''
44
+ return label.indexOf('Workspace actions') !== -1 ||
45
+ (label.indexOf('工作区') !== -1 && label.indexOf('的操作') !== -1)
46
+ }
47
+
48
+ function findWorkspaceRow(el) {
49
+ while (el && el !== document.body && el !== document.documentElement) {
50
+ if (el.getAttribute && el.getAttribute('role') === 'treeitem') return el
51
+ el = el.parentElement
52
+ }
53
+ return null
54
+ }
55
+
56
+ function looksLikePath(text) {
57
+ return /^([/~]|[a-zA-Z]:\\)/.test(text.trim())
58
+ }
59
+
60
+ function extractPathFromHoverCard(card) {
61
+ var ariaLabel = card.getAttribute('aria-label')
62
+ if (ariaLabel) {
63
+ var idx = ariaLabel.indexOf(': ')
64
+ if (idx !== -1) {
65
+ var realPath = ariaLabel.slice(idx + 2).trim()
66
+ if (looksLikePath(realPath)) return realPath
67
+ }
68
+ }
69
+ var pathEl = card.querySelector('[class*="hoverPath"]')
70
+ if (pathEl) return (pathEl.textContent || '').trim()
71
+ var divs = card.querySelectorAll('div')
72
+ for (var i = 0; i < divs.length; i++) {
73
+ var text = (divs[i].textContent || '').trim()
74
+ if (looksLikePath(text)) return text
75
+ }
76
+ return null
77
+ }
78
+
79
+ // Workspace cache populated from ctx.workspaces.list
80
+ var workspaceItems = []
81
+ var archivedSessionIds = []
82
+
83
+ function extractTitleFromAria(label) {
84
+ var m = label.match(/工作区“([^”]+)”的操作/)
85
+ if (m) return m[1]
86
+ m = label.match(/Workspace actions for (.+)/)
87
+ if (m) return m[1]
88
+ return null
89
+ }
90
+
91
+ function findWorkspaceItemByTitle(title) {
92
+ for (var i = 0; i < workspaceItems.length; i++) {
93
+ if (workspaceItems[i].title === title) return workspaceItems[i]
94
+ }
95
+ return null
96
+ }
97
+
98
+ function findWorkspaceItemById(id) {
99
+ for (var i = 0; i < workspaceItems.length; i++) {
100
+ if (workspaceItems[i].workspaceId === id) return workspaceItems[i]
101
+ }
102
+ return null
103
+ }
104
+
105
+ // Session cache populated from ctx.sessions.list
106
+ var sessionById = {}
107
+ var sessionIds = []
108
+ var sessionCurrent = null
109
+ var sessionPhase = 'idle'
110
+
111
+ // Pre-fetch the workspace path whenever a hover card appears.
112
+ var hoverObserver = new MutationObserver(function (mutations) {
113
+ for (var i = 0; i < mutations.length; i++) {
114
+ var added = mutations[i].addedNodes
115
+ for (var j = 0; j < added.length; j++) {
116
+ var node = added[j]
117
+ if (node.nodeType !== 1) continue
118
+ var style = window.getComputedStyle(node)
119
+ if (style.position !== 'fixed') continue
120
+ var className = typeof node.className === 'string' ? node.className : ''
121
+ var hasHoverPath = node.querySelector('[class*="hoverPath"]') !== null
122
+ if (className.indexOf('card') === -1 && !hasHoverPath) continue
123
+ var path = extractPathFromHoverCard(node)
124
+ if (!path) continue
125
+ var target = document.elementFromPoint(lastMouseX, lastMouseY)
126
+ var row = target ? findWorkspaceRow(target) : null
127
+ if (row) {
128
+ row.setAttribute('data-dsh-open-workspace-path', path)
129
+ var item = null
130
+ for (var k = 0; k < workspaceItems.length; k++) {
131
+ if (workspaceItems[k].path === path) { item = workspaceItems[k]; break }
132
+ }
133
+ if (item) row.setAttribute('data-dsh-open-workspace-id', item.workspaceId)
134
+ }
135
+ }
136
+ }
137
+ })
138
+
139
+ document.addEventListener('mousemove', function (e) {
140
+ lastMouseX = e.clientX
141
+ lastMouseY = e.clientY
142
+ }, true)
143
+
144
+ document.addEventListener('pointerdown', function (e) {
145
+ var target = e.target
146
+ if (!(target instanceof Element)) return
147
+ var btn = target.closest('button[aria-label]')
148
+ if (!btn) return
149
+ var label = btn.getAttribute('aria-label') || ''
150
+ var isSessionMenu = label.indexOf('Session actions') !== -1 ||
151
+ (label.indexOf('会话') !== -1 && label.indexOf('的操作') !== -1)
152
+ if (isSessionMenu) {
153
+ pendingWorkspace = null
154
+ return
155
+ }
156
+ if (!isWorkspaceMenuButton(btn)) return
157
+ var row = findWorkspaceRow(btn)
158
+ if (!row) return
159
+ var title = extractTitleFromAria(label)
160
+ var item = title ? findWorkspaceItemByTitle(title) : null
161
+ var path = (item && item.path)
162
+ || row.getAttribute('data-dsh-open-workspace-path')
163
+ || null
164
+ var workspaceId = (item && item.workspaceId)
165
+ || row.getAttribute('data-dsh-open-workspace-id')
166
+ || null
167
+ // 只在真正的单个工作区行菜单注入;跳过会话菜单和最顶层工作区菜单。
168
+ if (!workspaceId) return
169
+ pendingWorkspace = {
170
+ workspaceId: workspaceId,
171
+ path: path,
172
+ title: title || (item && item.title) || '',
173
+ expires: Date.now() + 10000
174
+ }
175
+ }, true)
176
+
177
+ // ---- Open path via Typert RPC over HTTP ----
178
+ function makeRpcId() {
179
+ return Date.now().toString(36) + Math.random().toString(36).slice(2)
180
+ }
181
+ function callOpenWorkspacePath(path) {
182
+ var token = typeof location !== 'undefined' ? new URLSearchParams(location.search).get('token') : null
183
+ var url = '/api/session/openWorkspacePath' + (token ? '?token=' + encodeURIComponent(token) : '')
184
+ var rpcId = makeRpcId()
185
+ return fetch(url, {
186
+ method: 'POST',
187
+ headers: { 'Content-Type': 'application/json' },
188
+ body: JSON.stringify({
189
+ type: 'client-request',
190
+ rpcId: rpcId,
191
+ method: 'session/openWorkspacePath',
192
+ payload: { args: { request: { path } } }
193
+ })
194
+ }).then(function (res) {
195
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' ' + res.statusText)
196
+ return res.json()
197
+ })
198
+ }
199
+
200
+ // ---- Batch archive dialog ----
201
+ var archiveOverlay = null
202
+ var archivePanel = null
203
+
204
+ function formatTime(t) {
205
+ if (!t) return ''
206
+ var d = new Date(t)
207
+ return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') + ' ' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0')
208
+ }
209
+
210
+ function template(fmt, vars) {
211
+ return fmt.replace(/\{([^{}]+)\}/g, function (_, key) {
212
+ return vars[key] !== undefined ? String(vars[key]) : ''
213
+ })
214
+ }
215
+
216
+ function closeArchiveDialog() {
217
+ if (archiveOverlay && archiveOverlay.parentNode) {
218
+ archiveOverlay.parentNode.removeChild(archiveOverlay)
219
+ }
220
+ archiveOverlay = null
221
+ archivePanel = null
222
+ }
223
+
224
+ function openArchiveDialog() {
225
+ closeArchiveDialog()
226
+ if (!pendingWorkspace || !pendingWorkspace.workspaceId) {
227
+ window.alert(LABELS.workspaceNotFound)
228
+ return
229
+ }
230
+ var item = findWorkspaceItemById(pendingWorkspace.workspaceId)
231
+ if (!item) {
232
+ window.alert(LABELS.workspaceNotFound)
233
+ return
234
+ }
235
+
236
+ var archivedSet = {}
237
+ for (var i = 0; i < archivedSessionIds.length; i++) archivedSet[archivedSessionIds[i]] = true
238
+
239
+ // 延迟读取 sessions 快照,避免应用启动/订阅时因 inactive context 抛错。
240
+ if (ctx.sessions && ctx.sessions.list) {
241
+ try {
242
+ var sessionSnap = ctx.sessions.list.getSnapshot()
243
+ sessionIds = sessionSnap.ids || []
244
+ sessionCurrent = sessionSnap.current || null
245
+ sessionById = sessionSnap.byId || {}
246
+ sessionPhase = sessionSnap.phase || 'idle'
247
+ } catch (err) {
248
+ // inactive context 等情况下静默跳过
249
+ }
250
+ }
251
+
252
+ var workspaceSessionIds = item.sessionIds || []
253
+ var candidates = []
254
+ for (var j = 0; j < workspaceSessionIds.length; j++) {
255
+ var sid = workspaceSessionIds[j]
256
+ var s = sessionById[sid]
257
+ if (!s) continue
258
+ if (s.blank) continue
259
+ if (archivedSet[sid]) continue
260
+ candidates.push({ id: sid, title: s.displayTitle || s.title || sid, updatedAt: s.updatedAt, running: s.running })
261
+ }
262
+
263
+ archiveOverlay = document.createElement('div')
264
+ archiveOverlay.className = 'dsh-batch-archive-overlay'
265
+ archivePanel = document.createElement('div')
266
+ archivePanel.className = 'dsh-batch-archive-panel'
267
+ archivePanel.setAttribute('role', 'dialog')
268
+ archivePanel.setAttribute('aria-modal', 'true')
269
+ archivePanel.setAttribute('aria-label', LABELS.batchArchiveTitle)
270
+
271
+ var header = document.createElement('div')
272
+ header.className = 'dsh-batch-archive-header'
273
+ var titleEl = document.createElement('div')
274
+ titleEl.className = 'dsh-batch-archive-title'
275
+ titleEl.textContent = LABELS.batchArchiveTitle + ' — ' + item.title
276
+ var closeBtn = document.createElement('button')
277
+ closeBtn.type = 'button'
278
+ closeBtn.className = 'dsh-batch-archive-close'
279
+ closeBtn.setAttribute('aria-label', LABELS.close)
280
+ closeBtn.textContent = '×'
281
+ closeBtn.addEventListener('click', closeArchiveDialog)
282
+ header.appendChild(titleEl)
283
+ header.appendChild(closeBtn)
284
+ archivePanel.appendChild(header)
285
+
286
+ var body = document.createElement('div')
287
+ body.className = 'dsh-batch-archive-body'
288
+
289
+ var selectedIds = []
290
+ var listContainer = null
291
+ var selectAllCheckbox = null
292
+ var statusEl = null
293
+ var archiveBtn = null
294
+
295
+ function updateState() {
296
+ if (statusEl) statusEl.textContent = template(LABELS.selectedCount, { n: selectedIds.length })
297
+ if (archiveBtn) archiveBtn.disabled = selectedIds.length === 0
298
+ if (selectAllCheckbox && listContainer) {
299
+ var boxes = listContainer.querySelectorAll('input[type="checkbox"][data-session-id]')
300
+ var allChecked = boxes.length > 0
301
+ for (var i = 0; i < boxes.length; i++) {
302
+ if (!boxes[i].checked) { allChecked = false; break }
303
+ }
304
+ selectAllCheckbox.checked = allChecked
305
+ }
306
+ }
307
+
308
+ if (candidates.length === 0) {
309
+ var emptyEl = document.createElement('div')
310
+ emptyEl.className = 'dsh-batch-archive-empty'
311
+ emptyEl.textContent = LABELS.noSessions
312
+ body.appendChild(emptyEl)
313
+ } else {
314
+ var toolbar = document.createElement('div')
315
+ toolbar.className = 'dsh-batch-archive-toolbar'
316
+ var selectAllLabel = document.createElement('label')
317
+ selectAllLabel.className = 'dsh-batch-archive-selectall'
318
+ selectAllCheckbox = document.createElement('input')
319
+ selectAllCheckbox.type = 'checkbox'
320
+ var selectAllText = document.createElement('span')
321
+ selectAllText.textContent = LABELS.selectAll
322
+ selectAllLabel.appendChild(selectAllCheckbox)
323
+ selectAllLabel.appendChild(selectAllText)
324
+ statusEl = document.createElement('span')
325
+ statusEl.className = 'dsh-batch-archive-status'
326
+ toolbar.appendChild(selectAllLabel)
327
+ toolbar.appendChild(statusEl)
328
+ body.appendChild(toolbar)
329
+
330
+ listContainer = document.createElement('div')
331
+ listContainer.className = 'dsh-batch-archive-list'
332
+ for (var c = 0; c < candidates.length; c++) {
333
+ (function (candidate) {
334
+ var row = document.createElement('label')
335
+ row.className = 'dsh-batch-archive-row'
336
+ if (candidate.running) row.className += ' dsh-batch-archive-running'
337
+ var checkbox = document.createElement('input')
338
+ checkbox.type = 'checkbox'
339
+ checkbox.setAttribute('data-session-id', candidate.id)
340
+ checkbox.addEventListener('change', function () {
341
+ var idx = selectedIds.indexOf(candidate.id)
342
+ if (checkbox.checked && idx === -1) selectedIds.push(candidate.id)
343
+ if (!checkbox.checked && idx !== -1) selectedIds.splice(idx, 1)
344
+ updateState()
345
+ })
346
+ var labelSpan = document.createElement('span')
347
+ labelSpan.className = 'dsh-batch-archive-name'
348
+ labelSpan.textContent = candidate.title
349
+ var timeSpan = document.createElement('span')
350
+ timeSpan.className = 'dsh-batch-archive-time'
351
+ timeSpan.textContent = formatTime(candidate.updatedAt)
352
+ row.appendChild(checkbox)
353
+ row.appendChild(labelSpan)
354
+ row.appendChild(timeSpan)
355
+ listContainer.appendChild(row)
356
+ })(candidates[c])
357
+ }
358
+ body.appendChild(listContainer)
359
+
360
+ selectAllCheckbox.addEventListener('change', function () {
361
+ var boxes = listContainer.querySelectorAll('input[type="checkbox"][data-session-id]')
362
+ for (var i = 0; i < boxes.length; i++) {
363
+ var sid = boxes[i].getAttribute('data-session-id')
364
+ boxes[i].checked = selectAllCheckbox.checked
365
+ var idx = selectedIds.indexOf(sid)
366
+ if (selectAllCheckbox.checked && idx === -1) selectedIds.push(sid)
367
+ if (!selectAllCheckbox.checked && idx !== -1) selectedIds.splice(idx, 1)
368
+ }
369
+ updateState()
370
+ })
371
+ }
372
+
373
+ archivePanel.appendChild(body)
374
+
375
+ var footer = document.createElement('div')
376
+ footer.className = 'dsh-batch-archive-footer'
377
+ archiveBtn = document.createElement('button')
378
+ archiveBtn.type = 'button'
379
+ archiveBtn.className = 'dsh-batch-archive-primary'
380
+ archiveBtn.textContent = LABELS.archiveSelected
381
+ archiveBtn.disabled = true
382
+ archiveBtn.addEventListener('click', function () {
383
+ if (selectedIds.length === 0) return
384
+ if (typeof ctx === 'undefined' || !ctx.uiWorkspace || typeof ctx.uiWorkspace.archiveSession !== 'function') {
385
+ window.alert(LABELS.archiveFailed + 'uiWorkspace.archiveSession not available')
386
+ return
387
+ }
388
+ archiveBtn.disabled = true
389
+ if (cancelBtn) cancelBtn.disabled = true
390
+ archiveBtn.textContent = LABELS.loading
391
+
392
+ var failedOnce = false
393
+ function archiveNext(i) {
394
+ if (i >= selectedIds.length) {
395
+ if (!failedOnce) {
396
+ window.alert(template(LABELS.archivedCount, { n: selectedIds.length }))
397
+ closeArchiveDialog()
398
+ } else {
399
+ archiveBtn.disabled = false
400
+ if (cancelBtn) cancelBtn.disabled = false
401
+ archiveBtn.textContent = LABELS.archiveSelected
402
+ }
403
+ return
404
+ }
405
+ ctx.uiWorkspace.archiveSession(selectedIds[i]).then(function () {
406
+ archiveNext(i + 1)
407
+ }).catch(function (err) {
408
+ failedOnce = true
409
+ console.error('[' + NS + '] batch archive error at ' + selectedIds[i] + ':', err)
410
+ window.alert(LABELS.archiveFailed + (err && err.message ? err.message : String(err)))
411
+ archiveBtn.disabled = false
412
+ if (cancelBtn) cancelBtn.disabled = false
413
+ archiveBtn.textContent = LABELS.archiveSelected
414
+ })
415
+ }
416
+ archiveNext(0)
417
+ })
418
+ var cancelBtn = document.createElement('button')
419
+ cancelBtn.type = 'button'
420
+ cancelBtn.className = 'dsh-batch-archive-secondary'
421
+ cancelBtn.textContent = LABELS.cancel
422
+ cancelBtn.addEventListener('click', closeArchiveDialog)
423
+ footer.appendChild(cancelBtn)
424
+ footer.appendChild(archiveBtn)
425
+ archivePanel.appendChild(footer)
426
+
427
+ archiveOverlay.appendChild(archivePanel)
428
+ document.body.appendChild(archiveOverlay)
429
+ updateState()
430
+
431
+ archiveOverlay.addEventListener('click', function (e) {
432
+ if (e.target === archiveOverlay) closeArchiveDialog()
433
+ })
434
+ }
435
+
436
+ // ---- Menu injection ----
437
+ var EXPLORER_ICON_SVG = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path><polyline points="2 12 12 12 16 8"></polyline></svg>'
438
+ var ARCHIVE_ICON_SVG = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8v13H3V8"></path><path d="M1 3h22v5H1z"></path><path d="M10 12h4"></path></svg>'
439
+
440
+ function makeMenuItem(ref, label, iconSvg, markerAttr, onClick, extraClass) {
441
+ var item = document.createElement('button')
442
+ item.type = 'button'
443
+ item.setAttribute('role', 'menuitem')
444
+ item.setAttribute(markerAttr, 'true')
445
+ item.className = ref.className + (extraClass ? ' ' + extraClass : '')
446
+ var refChildren = ref.children
447
+ var iconClass = refChildren[0] ? refChildren[0].className : ''
448
+ var labelClass = refChildren[1] ? refChildren[1].className : ''
449
+ var iconSpan = document.createElement('span')
450
+ if (iconClass) iconSpan.className = iconClass
451
+ iconSpan.innerHTML = iconSvg
452
+ var labelSpan = document.createElement('span')
453
+ if (labelClass) labelSpan.className = labelClass
454
+ labelSpan.textContent = label
455
+ item.appendChild(iconSpan)
456
+ item.appendChild(labelSpan)
457
+ item.addEventListener('click', function (e) {
458
+ e.stopPropagation()
459
+ onClick()
460
+ })
461
+ return item
462
+ }
463
+
464
+ function injectMenuItem(menu) {
465
+ if (menu.querySelector('[data-dsh-open-workspace-item]') && menu.querySelector('[data-dsh-batch-archive-item]')) return
466
+ var ref = menu.querySelector('[role="menuitem"]')
467
+ if (!ref) return
468
+
469
+ if (!menu.querySelector('[data-dsh-open-workspace-item]')) {
470
+ var explorerItem = makeMenuItem(ref, LABELS.openInExplorer, EXPLORER_ICON_SVG, 'data-dsh-open-workspace-item', function () {
471
+ console.log('[' + NS + '] open in explorer clicked, pendingWorkspace=', pendingWorkspace)
472
+ if (!pendingWorkspace || !pendingWorkspace.path) {
473
+ window.alert(LABELS.pathNotFound)
474
+ return
475
+ }
476
+ console.log('[' + NS + '] calling openWorkspacePath with path:', pendingWorkspace.path)
477
+ callOpenWorkspacePath(pendingWorkspace.path)
478
+ .then(function (result) {
479
+ console.log('[' + NS + '] openWorkspacePath result:', result)
480
+ var ok = result.ok !== false || result.opened === true
481
+ if (!ok) {
482
+ console.error('[' + NS + '] openWorkspacePath failed:', result.error)
483
+ window.alert(LABELS.openFailed + (result.error ? result.error.message : ''))
484
+ }
485
+ })
486
+ .catch(function (err) {
487
+ console.error('[' + NS + '] openWorkspacePath error:', err)
488
+ window.alert(LABELS.openFailed + (err && err.message ? err.message : String(err)))
489
+ })
490
+ })
491
+ if (ref.parentElement) ref.parentElement.insertBefore(explorerItem, ref)
492
+ else menu.appendChild(explorerItem)
493
+ }
494
+
495
+ if (!menu.querySelector('[data-dsh-batch-archive-item]')) {
496
+ var archiveItem = makeMenuItem(ref, LABELS.batchArchive, ARCHIVE_ICON_SVG, 'data-dsh-batch-archive-item', function () {
497
+ openArchiveDialog()
498
+ })
499
+ if (ref.parentElement) {
500
+ var sibling = menu.querySelector('[data-dsh-open-workspace-item]') || ref
501
+ if (sibling.nextSibling) ref.parentElement.insertBefore(archiveItem, sibling.nextSibling)
502
+ else ref.parentElement.appendChild(archiveItem)
503
+ } else {
504
+ menu.appendChild(archiveItem)
505
+ }
506
+ }
507
+ }
508
+
509
+ var menuObserver = new MutationObserver(function (mutations) {
510
+ if (!pendingWorkspace || Date.now() > pendingWorkspace.expires) return
511
+ for (var i = 0; i < mutations.length; i++) {
512
+ var added = mutations[i].addedNodes
513
+ for (var j = 0; j < added.length; j++) {
514
+ var node = added[j]
515
+ if (node.nodeType !== 1) continue
516
+ var menu = (node.getAttribute && node.getAttribute('role') === 'menu') ? node : null
517
+ if (!menu && node.querySelector) menu = node.querySelector('[role="menu"]')
518
+ if (menu) injectMenuItem(menu)
519
+ }
520
+ }
521
+ })
522
+
523
+ // ---- Styles for the archive dialog ----
524
+ function injectStyles() {
525
+ if (document.getElementById('dsh-batch-archive-styles')) return
526
+ var style = document.createElement('style')
527
+ style.id = 'dsh-batch-archive-styles'
528
+ style.textContent = [
529
+ '.dsh-batch-archive-overlay { position: fixed; inset: 0; background: var(--dsw-alias-bg-mask-1, rgba(0,0,0,0.45)); z-index: 2147483640; display: flex; align-items: center; justify-content: center; }',
530
+ '.dsh-batch-archive-panel { background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #1f2328); border: 1px solid var(--dsw-alias-border-l1, rgba(0,0,0,0.1)); border-radius: 14px; box-shadow: var(--dsw-elevation-prominent, 0 10px 40px rgba(0,0,0,0.25)); width: min(480px, calc(100vw - 32px)); max-height: min(640px, calc(100vh - 48px)); display: flex; flex-direction: column; overflow: hidden; }',
531
+ '.dsh-batch-archive-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; border-bottom: 1px solid var(--dsw-alias-border-l2, #e5e7eb); }',
532
+ '.dsh-batch-archive-title { font-size: 15px; font-weight: 600; line-height: 1.35; color: var(--dsw-alias-label-primary, inherit); }',
533
+ '.dsh-batch-archive-close { background: transparent; border: none; color: var(--dsw-alias-label-tertiary, #6b7280); font-size: 20px; line-height: 1; cursor: pointer; padding: 2px 6px; border-radius: 6px; }',
534
+ '.dsh-batch-archive-close:hover { color: var(--dsw-alias-label-primary, #111827); background: var(--dsw-alias-interactive-bg-hover, transparent); }',
535
+ '.dsh-batch-archive-body { padding: 12px 18px; overflow-y: auto; flex: 1; }',
536
+ '.dsh-batch-archive-empty { padding: 24px 0; text-align: center; color: var(--dsw-alias-label-secondary, #6b7280); }',
537
+ '.dsh-batch-archive-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; font-size: 13px; color: var(--dsw-alias-label-secondary, #6b7280); }',
538
+ '.dsh-batch-archive-selectall { display: flex; align-items: center; gap: 6px; cursor: pointer; color: var(--dsw-alias-label-secondary, #374151); }',
539
+ '.dsh-batch-archive-selectall input[type="checkbox"] { accent-color: var(--dsw-alias-brand-primary, #2563eb); width: 14px; height: 14px; margin: 0; }',
540
+ '.dsh-batch-archive-status { color: var(--dsw-alias-label-tertiary, #6b7280); }',
541
+ '.dsh-batch-archive-list { display: flex; flex-direction: column; gap: 2px; }',
542
+ '.dsh-batch-archive-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 8px; cursor: pointer; user-select: none; font-size: 13px; color: var(--dsw-alias-label-primary, inherit); }',
543
+ '.dsh-batch-archive-row:hover { background: var(--dsw-alias-interactive-bg-hover, #f3f4f6); }',
544
+ '.dsh-batch-archive-row.dsh-batch-archive-running { opacity: 0.6; }',
545
+ '.dsh-batch-archive-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
546
+ '.dsh-batch-archive-time { color: var(--dsw-alias-label-tertiary, #6b7280); font-size: 12px; flex-shrink: 0; }',
547
+ '.dsh-batch-archive-footer { display: flex; justify-content: flex-end; gap: 10px; padding: 14px 18px; border-top: 1px solid var(--dsw-alias-border-l2, #e5e7eb); }',
548
+ '.dsh-batch-archive-primary, .dsh-batch-archive-secondary { padding: 7px 14px; border-radius: 8px; font-size: 13px; cursor: pointer; border: 1px solid transparent; }',
549
+ '.dsh-batch-archive-primary { background: var(--dsw-alias-button-primary-fill, #111827); color: var(--dsw-alias-label-primary-foreground, #fff); }',
550
+ '.dsh-batch-archive-primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover, #374151); }',
551
+ '.dsh-batch-archive-primary:disabled { opacity: 0.5; cursor: not-allowed; }',
552
+ '.dsh-batch-archive-secondary { background: transparent; color: var(--dsw-alias-label-secondary, #374151); border-color: var(--dsw-alias-border-l2, #d1d5db); }',
553
+ '.dsh-batch-archive-secondary:hover { background: var(--dsw-alias-interactive-bg-hover, #f3f4f6); }'
554
+ ].join('\n')
555
+ document.head.appendChild(style)
556
+ }
557
+
558
+ // Handle Escape to close the dialog.
559
+ document.addEventListener('keydown', function (e) {
560
+ if (e.key === 'Escape' && archiveOverlay) {
561
+ e.stopPropagation()
562
+ closeArchiveDialog()
563
+ }
564
+ }, true)
565
+
566
+ // ---- Plugin entry ----
567
+ var ctx = null
568
+
569
+ function apply(pluginCtx) {
570
+ ctx = pluginCtx
571
+ injectStyles()
572
+ if (ctx.workspaces && ctx.workspaces.list) {
573
+ try {
574
+ var refreshWorkspaces = function () {
575
+ try {
576
+ var snap = ctx.workspaces.list.getSnapshot()
577
+ workspaceItems = snap.items.map(function (w) {
578
+ return { workspaceId: w.workspaceId, path: w.path, title: w.title, sessionIds: w.sessionIds || [] }
579
+ })
580
+ archivedSessionIds = snap.archivedSessionIds || []
581
+ } catch (err) {
582
+ console.warn('[' + NS + '] failed to read workspaces snapshot:', err)
583
+ }
584
+ }
585
+ refreshWorkspaces()
586
+ ctx.workspaces.list.subscribe(refreshWorkspaces)
587
+ } catch (err) {
588
+ console.warn('[' + NS + '] workspaces service not usable:', err)
589
+ }
590
+ } else {
591
+ console.warn('[' + NS + '] ctx.workspaces not available')
592
+ }
593
+
594
+ // sessions/uiWorkspace 只在批量归档弹窗中使用;快照延迟到打开弹窗时读取,
595
+ // 避免在 inactive context 中反复订阅、刷屏报错。
596
+
597
+ hoverObserver.observe(document.body, { childList: true, subtree: true })
598
+ menuObserver.observe(document.body, { childList: true, subtree: true })
599
+ console.log('[' + NS + '] initialized, workspaces:', workspaceItems.length, 'sessions:', sessionIds.length)
600
+ }
601
+
602
+ exports.apply = apply
603
+ exports.inject = ['workspaces', 'sessions', 'uiWorkspace']
604
+ return module.exports
605
+ }
606
+ })
@@ -0,0 +1,7 @@
1
+ # dsh-workspace-toolkit bundle patch
2
+ # Injects the browser-half plugin into the Cordis profile.
3
+
4
+ - insert:
5
+ - id: workspace-toolkit
6
+ name: dsh-workspace-toolkit
7
+ config: {}
package/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // Host half of dsh-workspace-toolkit.
2
+ // DSH already exposes session.openWorkspacePath, so no host API is needed.
3
+
4
+ export const name = 'workspace-toolkit'
5
+
6
+ export function apply() {
7
+ // No server state to register.
8
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "dsh-workspace-toolkit",
3
+ "version": "0.0.2",
4
+ "description": "DSH 工作区增强工具包:在资源管理器中打开、批量归档会话,以及后续文件/会话操作功能",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./client": "./client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "dsh": {
13
+ "bundle": {
14
+ "patch": "./cordis.patch.yml"
15
+ },
16
+ "client": {
17
+ "platform": "web",
18
+ "immediately": true,
19
+ "inject": []
20
+ }
21
+ },
22
+ "files": [
23
+ "index.js",
24
+ "client.js",
25
+ "cordis.patch.yml",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/gege9527/dsh-workspace-toolkit.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/gege9527/dsh-workspace-toolkit/issues"
35
+ },
36
+ "homepage": "https://github.com/gege9527/dsh-workspace-toolkit#readme",
37
+ "license": "MIT",
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "peerDependencies": {
42
+ "@deepseek-ai/dsh-tools": ">=0.0.1-rc.1"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "@deepseek-ai/dsh-tools": {
46
+ "optional": false
47
+ }
48
+ },
49
+ "keywords": [
50
+ "dsh",
51
+ "deepseek",
52
+ "dsh-plugin",
53
+ "workspace",
54
+ "file-explorer",
55
+ "session",
56
+ "archive"
57
+ ]
58
+ }