dsh-command-palette 0.1.0 → 0.1.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/README.en.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm](https://img.shields.io/npm/v/dsh-command-palette)](https://www.npmjs.com/package/dsh-command-palette)
6
6
  [![stars](https://img.shields.io/github/stars/chenyangcun/dsh-command-palette?style=flat)](https://github.com/chenyangcun/dsh-command-palette)
7
7
 
8
- A keyboard-first command palette for standard DeepSeek Harness (DSH). Double-tap Shift to search and open sessions, workspaces, available features, or reusable custom commands.
8
+ A keyboard-first command palette for standard DeepSeek Harness (DSH). Double-tap Shift to search and open sessions, workspaces, settings pages, available features, or reusable custom commands.
9
9
 
10
10
  ## Install
11
11
 
@@ -21,6 +21,7 @@ Restart the current DSH Web process after installation, then open **Settings →
21
21
  - **Recent sessions** — shows the six most recently updated valid sessions by default
22
22
  - **Session search** — searches titles, working directories, Agent presets, and session IDs
23
23
  - **Workspace switching** — starts a session in the selected workspace directly from the palette
24
+ - **Settings navigation** — discovers registered DSH settings sections and opens the selected page directly
24
25
  - **Custom commands** — saves frequently used prompts and either creates a session each time or reuses a dedicated session
25
26
  - **Workspace binding** — chooses the workspace used when a custom command creates a session
26
27
  - **Provider API** — lets other Client plugins register commands or trigger the palette
@@ -32,6 +33,8 @@ Restart the current DSH Web process after installation, then open **Settings →
32
33
  3. Use `↑` / `↓` to select, Enter to run, and Esc to close.
33
34
  4. Add, edit, or remove custom commands under **Settings → Command Palette**.
34
35
 
36
+ The palette reads the current settings-section registry whenever it opens, so settings pages added by other plugins automatically appear in the **Settings** group.
37
+
35
38
  When **Reuse dedicated session** is enabled, the first run creates and stores a session binding. Later runs continue in that session. If the bound session is archived or unavailable, the plugin creates a replacement automatically.
36
39
 
37
40
  ## Standard DSH and extension API
@@ -75,6 +78,16 @@ Service API:
75
78
 
76
79
  Provider IDs must be unique. Commands require at least `id`, `title`, and `run()`; the provider ID is automatically prefixed to the final command ID.
77
80
 
81
+ Dynamic providers may also implement `subscribe(invalidate)`. Call `invalidate()` when external state changes to make the palette collect commands again. The disposer returned by `subscribe` runs automatically when the provider is removed:
82
+
83
+ ```js
84
+ {
85
+ id: 'dynamic-plugin',
86
+ collect: () => buildCommands(currentState),
87
+ subscribe: (invalidate) => externalStore.subscribe(() => invalidate())
88
+ }
89
+ ```
90
+
78
91
  Native Desktop browser tabs, Electron IPC, and native-view coordination are outside this package. A separate Desktop enhancement plugin can add them later through this API.
79
92
 
80
93
  ## Data storage
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm](https://img.shields.io/npm/v/dsh-command-palette)](https://www.npmjs.com/package/dsh-command-palette)
6
6
  [![stars](https://img.shields.io/github/stars/chenyangcun/dsh-command-palette?style=flat)](https://github.com/chenyangcun/dsh-command-palette)
7
7
 
8
- 适用于标准版 DeepSeek Harness(DSH)的键盘优先命令面板。连续快速按两次 Shift,即可搜索和打开会话、工作区、已安装功能或可复用的自定义指令。
8
+ 适用于标准版 DeepSeek Harness(DSH)的键盘优先命令面板。连续快速按两次 Shift,即可搜索和打开会话、工作区、设置页面、已安装功能或可复用的自定义指令。
9
9
 
10
10
  ## 安装
11
11
 
@@ -21,6 +21,7 @@ dsh plugin --profile web add dsh-command-palette
21
21
  - **最近会话**——默认展示最近更新的六个有效会话
22
22
  - **会话搜索**——按标题、工作目录、Agent 预设或会话 ID 搜索
23
23
  - **工作区切换**——从命令面板直接在目标工作区启动会话
24
+ - **设置页跳转**——自动读取 DSH 已注册的设置菜单,搜索后直接打开对应设置页面
24
25
  - **自定义指令**——保存常用 Prompt,可选择每次新建会话或复用专属会话
25
26
  - **工作区绑定**——为自定义指令的新会话指定目标工作区
26
27
  - **扩展 Provider**——其他 Client 插件可以注册自己的命令或触发面板
@@ -32,6 +33,8 @@ dsh plugin --profile web add dsh-command-palette
32
33
  3. 使用 `↑` / `↓` 选择,按 Enter 执行,按 Esc 关闭。
33
34
  4. 在 **设置 → 命令面板** 中添加、编辑或删除自定义指令。
34
35
 
36
+ 插件会在每次打开面板时读取当前已注册的设置页面,因此其他插件新增的设置菜单也会自动出现在“设置”分组中。
37
+
35
38
  自定义指令启用“复用专属会话”后,首次执行会创建会话并保存绑定;后续执行继续向同一会话发送内容。如果绑定会话已经归档或不可用,插件会自动创建新会话。
36
39
 
37
40
  ## 标准 DSH 与扩展接口
@@ -75,6 +78,16 @@ exports.apply = (ctx) => {
75
78
 
76
79
  Provider ID 必须唯一。命令至少需要 `id`、`title` 和 `run()`;最终命令 ID 会自动加上 Provider 前缀。
77
80
 
81
+ 动态 Provider 可以额外实现 `subscribe(invalidate)`。当外部状态变化时调用 `invalidate()`,命令面板会重新收集命令;`subscribe` 返回的卸载函数会在 Provider 注销时自动执行:
82
+
83
+ ```js
84
+ {
85
+ id: 'dynamic-plugin',
86
+ collect: () => buildCommands(currentState),
87
+ subscribe: (invalidate) => externalStore.subscribe(() => invalidate())
88
+ }
89
+ ```
90
+
78
91
  Desktop 原生浏览器标签、Electron IPC 和原生视图协调不属于本包,后续可以由独立 Desktop 增强插件通过上述接口接入。
79
92
 
80
93
  ## 数据存储
package/client.js CHANGED
@@ -7,8 +7,8 @@ window.__ModuleLoader__.load({
7
7
  const React = require('react')
8
8
  const ReactDOM = require('react-dom')
9
9
 
10
- const GROUP_ORDER = ['features', 'browser', 'workspaces', 'sessions']
11
- const GROUP_LABELS = { features: '功能', browser: '浏览器', workspaces: '工作区', sessions: '会话', recent: '最近会话' }
10
+ const GROUP_ORDER = ['features', 'settings', 'browser', 'workspaces', 'sessions']
11
+ const GROUP_LABELS = { features: '功能', settings: '设置', browser: '浏览器', workspaces: '工作区', sessions: '会话', recent: '最近会话' }
12
12
  const SUPPORTED_GROUPS = new Set(GROUP_ORDER)
13
13
 
14
14
  function normalizeCustomCommand(value) {
@@ -36,7 +36,10 @@ window.__ModuleLoader__.load({
36
36
  let revision = 0
37
37
  const emit = (reason) => {
38
38
  const snapshot = { open, revision: ++revision, reason }
39
- for (const listener of listeners) listener(snapshot)
39
+ for (const listener of listeners) {
40
+ try { listener(snapshot) }
41
+ catch (error) { console.warn('[dsh-command-palette] subscriber failed:', error) }
42
+ }
40
43
  }
41
44
  return {
42
45
  registerProvider(provider) {
@@ -45,15 +48,33 @@ window.__ModuleLoader__.load({
45
48
  }
46
49
  const id = provider.id.trim()
47
50
  if (providers.has(id)) throw new Error(`Command palette provider already registered: ${id}`)
48
- providers.set(id, provider)
51
+ const record = { provider, disposeSubscription: null }
52
+ providers.set(id, record)
49
53
  emit('provider-registered')
54
+ if (typeof provider.subscribe === 'function') {
55
+ try {
56
+ const dispose = provider.subscribe(() => emit('provider-updated'))
57
+ if (typeof dispose === 'function') record.disposeSubscription = dispose
58
+ } catch (error) {
59
+ console.warn(`[dsh-command-palette] provider ${id} subscription failed:`, error)
60
+ }
61
+ }
62
+ let removed = false
50
63
  return () => {
51
- if (providers.delete(id)) emit('provider-removed')
64
+ if (removed) return
65
+ removed = true
66
+ const current = providers.get(id)
67
+ if (!current) return
68
+ providers.delete(id)
69
+ try { current.disposeSubscription?.() }
70
+ catch (error) { console.warn(`[dsh-command-palette] provider ${id} disposer failed:`, error) }
71
+ emit('provider-removed')
52
72
  }
53
73
  },
54
74
  collect(context = {}) {
55
75
  const commands = []
56
- for (const [providerId, provider] of providers) {
76
+ for (const [providerId, record] of providers) {
77
+ const provider = record.provider
57
78
  const commandIds = new Set()
58
79
  let provided
59
80
  try { provided = provider.collect(context) }
@@ -106,6 +127,69 @@ window.__ModuleLoader__.load({
106
127
  return [command.title, command.subtitle, ...(command.keywords || [])].filter(Boolean).join(' ').toLocaleLowerCase()
107
128
  }
108
129
 
130
+ function resolveSettingsLabel(value) {
131
+ try {
132
+ const label = typeof value === 'function' ? value() : value
133
+ return typeof label === 'string' ? label.trim() : ''
134
+ } catch {
135
+ return ''
136
+ }
137
+ }
138
+
139
+ function findSettingsNavigationButton(label) {
140
+ for (const dialog of document.querySelectorAll('[role="dialog"]')) {
141
+ const nav = dialog.querySelector('nav')
142
+ if (!nav) continue
143
+ const button = Array.from(nav.querySelectorAll('button')).find((item) => item.textContent?.trim() === label)
144
+ if (button) return button
145
+ }
146
+ return null
147
+ }
148
+
149
+ async function openSettingsSection(label) {
150
+ let button = findSettingsNavigationButton(label)
151
+ if (!button) {
152
+ const trigger = document.querySelector('button[aria-haspopup="dialog"][aria-expanded]')
153
+ if (!trigger) throw new Error('当前 DSH 页面未提供可用的设置入口。')
154
+ trigger.click()
155
+ button = findSettingsNavigationButton(label)
156
+ for (let attempt = 0; !button && attempt < 4; attempt += 1) {
157
+ await new Promise((resolve) => requestAnimationFrame(resolve))
158
+ button = findSettingsNavigationButton(label)
159
+ }
160
+ }
161
+ if (!button) throw new Error(`无法打开设置页面:${label}`)
162
+ button.click()
163
+ }
164
+
165
+ function createSettingsProvider(ctx) {
166
+ return {
167
+ id: 'settings',
168
+ collect: () => {
169
+ let entries
170
+ try { entries = ctx.slots.entries('settings.section') }
171
+ catch { return [] }
172
+ return Array.from(entries || [])
173
+ .map((entry) => ({
174
+ id: typeof entry?.options?.id === 'string' ? entry.options.id.trim() : '',
175
+ order: typeof entry?.options?.order === 'number' ? entry.options.order : 0,
176
+ title: resolveSettingsLabel(entry?.options?.label)
177
+ }))
178
+ .filter(({ id, title }) => id && title)
179
+ .sort((left, right) => left.order - right.order)
180
+ .map(({ id, title }) => ({
181
+ id,
182
+ group: 'settings',
183
+ icon: '⚙',
184
+ title,
185
+ subtitle: '打开设置页面',
186
+ keywords: ['settings', '设置', id],
187
+ run: () => openSettingsSection(title)
188
+ }))
189
+ }
190
+ }
191
+ }
192
+
109
193
  function recentTimestamp(value) {
110
194
  return typeof value === 'number' ? value : Date.parse(String(value || '')) || 0
111
195
  }
@@ -358,6 +442,7 @@ window.__ModuleLoader__.load({
358
442
  const commandSettings = ctx.get('settingsScope').bind({ namespace: 'command-palette' })
359
443
  const paletteService = createCommandPaletteService()
360
444
  ctx.provide('commandPalette', paletteService)
445
+ const removeSettingsProvider = paletteService.registerProvider(createSettingsProvider(ctx))
361
446
  installStyles()
362
447
  const saveCustomCommands = async (next) => {
363
448
  const commands = normalizeCustomCommands(next)
@@ -399,6 +484,7 @@ window.__ModuleLoader__.load({
399
484
  ctx.slots.inject('shell.overlay', () => ctx.slots.register({ name: 'shell.overlay', id: 'dsh-command-palette', order: 220, inject: injected }, Palette))
400
485
  ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'command-palette-settings', order: 90, label: () => '命令面板', inject: injected }, ShortcutSettings))
401
486
  ctx.effect(() => () => {
487
+ removeSettingsProvider()
402
488
  paletteService.close()
403
489
  document.querySelector('style[data-plugin-css="dsh-command-palette"]')?.remove()
404
490
  }, 'dsh-command-palette: cleanup')
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-command-palette",
3
- "version": "0.1.0",
4
- "description": "A keyboard-first command palette for DeepSeek Harness sessions, workspaces, features, and reusable custom commands.",
3
+ "version": "0.1.2",
4
+ "description": "A keyboard-first command palette for DeepSeek Harness sessions, workspaces, settings, features, and reusable custom commands.",
5
5
  "license": "MIT",
6
6
  "author": "chenyangcun",
7
7
  "type": "module",