dsh-command-palette 0.1.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/LICENSE +21 -0
- package/README.en.md +102 -0
- package/README.md +102 -0
- package/client.js +412 -0
- package/cordis.patch.yml +3 -0
- package/index.js +24 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 chenyangcun
|
|
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.en.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# dsh-command-palette
|
|
2
|
+
|
|
3
|
+
[中文](README.md) | English
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-command-palette)
|
|
6
|
+
[](https://github.com/chenyangcun/dsh-command-palette)
|
|
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.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
dsh plugin --profile web add dsh-command-palette
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Restart the current DSH Web process after installation, then open **Settings → Command Palette** to manage custom commands.
|
|
17
|
+
|
|
18
|
+
## What you get
|
|
19
|
+
|
|
20
|
+
- **Double-Shift trigger** — listens only in the focused DSH Web window and does not register a system-wide shortcut
|
|
21
|
+
- **Recent sessions** — shows the six most recently updated valid sessions by default
|
|
22
|
+
- **Session search** — searches titles, working directories, Agent presets, and session IDs
|
|
23
|
+
- **Workspace switching** — starts a session in the selected workspace directly from the palette
|
|
24
|
+
- **Custom commands** — saves frequently used prompts and either creates a session each time or reuses a dedicated session
|
|
25
|
+
- **Workspace binding** — chooses the workspace used when a custom command creates a session
|
|
26
|
+
- **Provider API** — lets other Client plugins register commands or trigger the palette
|
|
27
|
+
|
|
28
|
+
## Use
|
|
29
|
+
|
|
30
|
+
1. Double-tap Shift inside the DSH Web window.
|
|
31
|
+
2. Type to filter commands.
|
|
32
|
+
3. Use `↑` / `↓` to select, Enter to run, and Esc to close.
|
|
33
|
+
4. Add, edit, or remove custom commands under **Settings → Command Palette**.
|
|
34
|
+
|
|
35
|
+
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
|
+
|
|
37
|
+
## Standard DSH and extension API
|
|
38
|
+
|
|
39
|
+
This package does not depend on Electron, a DSH Desktop preload, or a `window.dshDesktopCommandPalette` global. Sessions, workspaces, settings, and UI use standard DSH Client services.
|
|
40
|
+
|
|
41
|
+
The plugin provides a `commandPalette` service in the Client Context. Enhancement plugins can register additional commands:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
exports.inject = ['commandPalette']
|
|
45
|
+
|
|
46
|
+
exports.apply = (ctx) => {
|
|
47
|
+
ctx.inject(['commandPalette'], (scope) => {
|
|
48
|
+
scope.effect(() => scope.commandPalette.registerProvider({
|
|
49
|
+
id: 'my-plugin',
|
|
50
|
+
collect: () => [{
|
|
51
|
+
id: 'open-dashboard',
|
|
52
|
+
group: 'features',
|
|
53
|
+
icon: '⌘',
|
|
54
|
+
title: 'Open dashboard',
|
|
55
|
+
subtitle: 'Provided by an enhancement plugin',
|
|
56
|
+
keywords: ['dashboard'],
|
|
57
|
+
run: () => openDashboard()
|
|
58
|
+
}]
|
|
59
|
+
}))
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Service API:
|
|
65
|
+
|
|
66
|
+
| Method | Purpose |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `registerProvider(provider)` | Registers a command provider and returns a disposer |
|
|
69
|
+
| `collect(context?)` | Collects commands from every registered provider |
|
|
70
|
+
| `open()` | Opens the palette |
|
|
71
|
+
| `close()` | Closes the palette |
|
|
72
|
+
| `toggle()` | Toggles the palette |
|
|
73
|
+
| `isOpen()` | Returns the current open state |
|
|
74
|
+
| `subscribe(listener)` | Observes open-state or provider changes through `{ open, revision, reason }` snapshots |
|
|
75
|
+
|
|
76
|
+
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
|
+
|
|
78
|
+
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
|
+
|
|
80
|
+
## Data storage
|
|
81
|
+
|
|
82
|
+
Custom commands are stored through the DSH settings system under `command-palette.commands`. The plugin creates no separate database and reads no model credentials.
|
|
83
|
+
|
|
84
|
+
## Compatibility
|
|
85
|
+
|
|
86
|
+
- Standard DeepSeek Harness (DSH)
|
|
87
|
+
- `@deepseek-ai/dsh` `0.1.1-rc.2`
|
|
88
|
+
- The standard Web Profile
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
npm test
|
|
94
|
+
npm run check
|
|
95
|
+
npm pack --dry-run
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
[MIT](LICENSE)
|
|
101
|
+
|
|
102
|
+
Bug reports and suggestions are welcome in [Issues](https://github.com/chenyangcun/dsh-command-palette/issues).
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# dsh-command-palette
|
|
2
|
+
|
|
3
|
+
中文 | [English](README.en.md)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-command-palette)
|
|
6
|
+
[](https://github.com/chenyangcun/dsh-command-palette)
|
|
7
|
+
|
|
8
|
+
适用于标准版 DeepSeek Harness(DSH)的键盘优先命令面板。连续快速按两次 Shift,即可搜索和打开会话、工作区、已安装功能或可复用的自定义指令。
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
dsh plugin --profile web add dsh-command-palette
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
安装后重启当前 DSH Web 进程,然后打开 **设置 → 命令面板** 管理自定义指令。
|
|
17
|
+
|
|
18
|
+
## 功能
|
|
19
|
+
|
|
20
|
+
- **双 Shift 唤起**——只监听当前获得焦点的 DSH Web 窗口,不注册系统级全局快捷键
|
|
21
|
+
- **最近会话**——默认展示最近更新的六个有效会话
|
|
22
|
+
- **会话搜索**——按标题、工作目录、Agent 预设或会话 ID 搜索
|
|
23
|
+
- **工作区切换**——从命令面板直接在目标工作区启动会话
|
|
24
|
+
- **自定义指令**——保存常用 Prompt,可选择每次新建会话或复用专属会话
|
|
25
|
+
- **工作区绑定**——为自定义指令的新会话指定目标工作区
|
|
26
|
+
- **扩展 Provider**——其他 Client 插件可以注册自己的命令或触发面板
|
|
27
|
+
|
|
28
|
+
## 使用
|
|
29
|
+
|
|
30
|
+
1. 在 DSH Web 窗口内连续快速按两次 Shift。
|
|
31
|
+
2. 输入关键词筛选命令。
|
|
32
|
+
3. 使用 `↑` / `↓` 选择,按 Enter 执行,按 Esc 关闭。
|
|
33
|
+
4. 在 **设置 → 命令面板** 中添加、编辑或删除自定义指令。
|
|
34
|
+
|
|
35
|
+
自定义指令启用“复用专属会话”后,首次执行会创建会话并保存绑定;后续执行继续向同一会话发送内容。如果绑定会话已经归档或不可用,插件会自动创建新会话。
|
|
36
|
+
|
|
37
|
+
## 标准 DSH 与扩展接口
|
|
38
|
+
|
|
39
|
+
本包不依赖 Electron、DSH Desktop preload 或任何 `window.dshDesktopCommandPalette` 全局对象。会话、工作区、设置和 UI 均通过标准 DSH Client 服务实现。
|
|
40
|
+
|
|
41
|
+
插件在 Client Context 中提供 `commandPalette` 服务,后续增强插件可以注册附加命令:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
exports.inject = ['commandPalette']
|
|
45
|
+
|
|
46
|
+
exports.apply = (ctx) => {
|
|
47
|
+
ctx.inject(['commandPalette'], (scope) => {
|
|
48
|
+
scope.effect(() => scope.commandPalette.registerProvider({
|
|
49
|
+
id: 'my-plugin',
|
|
50
|
+
collect: () => [{
|
|
51
|
+
id: 'open-dashboard',
|
|
52
|
+
group: 'features',
|
|
53
|
+
icon: '⌘',
|
|
54
|
+
title: '打开控制台',
|
|
55
|
+
subtitle: '由扩展插件提供',
|
|
56
|
+
keywords: ['dashboard'],
|
|
57
|
+
run: () => openDashboard()
|
|
58
|
+
}]
|
|
59
|
+
}))
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
服务接口:
|
|
65
|
+
|
|
66
|
+
| 方法 | 说明 |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `registerProvider(provider)` | 注册命令 Provider,返回卸载函数 |
|
|
69
|
+
| `collect(context?)` | 收集当前所有扩展命令 |
|
|
70
|
+
| `open()` | 打开命令面板 |
|
|
71
|
+
| `close()` | 关闭命令面板 |
|
|
72
|
+
| `toggle()` | 切换命令面板 |
|
|
73
|
+
| `isOpen()` | 返回当前打开状态 |
|
|
74
|
+
| `subscribe(listener)` | 监听打开状态或 Provider 变化,接收 `{ open, revision, reason }` 快照 |
|
|
75
|
+
|
|
76
|
+
Provider ID 必须唯一。命令至少需要 `id`、`title` 和 `run()`;最终命令 ID 会自动加上 Provider 前缀。
|
|
77
|
+
|
|
78
|
+
Desktop 原生浏览器标签、Electron IPC 和原生视图协调不属于本包,后续可以由独立 Desktop 增强插件通过上述接口接入。
|
|
79
|
+
|
|
80
|
+
## 数据存储
|
|
81
|
+
|
|
82
|
+
自定义指令通过 DSH 设置系统保存在 `command-palette.commands`,不单独创建数据库或读取模型凭据。
|
|
83
|
+
|
|
84
|
+
## 兼容性
|
|
85
|
+
|
|
86
|
+
- 标准版 DeepSeek Harness(DSH)
|
|
87
|
+
- `@deepseek-ai/dsh` `0.1.1-rc.2`
|
|
88
|
+
- 标准 Web Profile
|
|
89
|
+
|
|
90
|
+
## 开发验证
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
npm test
|
|
94
|
+
npm run check
|
|
95
|
+
npm pack --dry-run
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 许可证
|
|
99
|
+
|
|
100
|
+
[MIT](LICENSE)
|
|
101
|
+
|
|
102
|
+
欢迎通过 [Issues](https://github.com/chenyangcun/dsh-command-palette/issues) 反馈问题或建议。
|
package/client.js
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: 'dsh-command-palette',
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
const module = { exports: {} }
|
|
5
|
+
const exports = module.exports
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
7
|
+
const React = require('react')
|
|
8
|
+
const ReactDOM = require('react-dom')
|
|
9
|
+
|
|
10
|
+
const GROUP_ORDER = ['features', 'browser', 'workspaces', 'sessions']
|
|
11
|
+
const GROUP_LABELS = { features: '功能', browser: '浏览器', workspaces: '工作区', sessions: '会话', recent: '最近会话' }
|
|
12
|
+
const SUPPORTED_GROUPS = new Set(GROUP_ORDER)
|
|
13
|
+
|
|
14
|
+
function normalizeCustomCommand(value) {
|
|
15
|
+
if (!value || typeof value !== 'object') return null
|
|
16
|
+
const name = typeof value.name === 'string' ? value.name.trim() : ''
|
|
17
|
+
const prompt = typeof value.prompt === 'string' ? value.prompt.trim() : ''
|
|
18
|
+
if (!name || !prompt) return null
|
|
19
|
+
return {
|
|
20
|
+
id: typeof value.id === 'string' && value.id ? value.id : crypto.randomUUID(),
|
|
21
|
+
name: name.slice(0, 80), prompt: prompt.slice(0, 12000),
|
|
22
|
+
sticky: value.sticky !== false,
|
|
23
|
+
...(typeof value.workspaceId === 'string' && value.workspaceId ? { workspaceId: value.workspaceId } : {}),
|
|
24
|
+
...(typeof value.stickySessionId === 'string' && value.stickySessionId ? { stickySessionId: value.stickySessionId } : {})
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeCustomCommands(values) {
|
|
29
|
+
return (Array.isArray(values) ? values : []).map(normalizeCustomCommand).filter(Boolean).slice(0, 40)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createCommandPaletteService() {
|
|
33
|
+
const providers = new Map()
|
|
34
|
+
const listeners = new Set()
|
|
35
|
+
let open = false
|
|
36
|
+
let revision = 0
|
|
37
|
+
const emit = (reason) => {
|
|
38
|
+
const snapshot = { open, revision: ++revision, reason }
|
|
39
|
+
for (const listener of listeners) listener(snapshot)
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
registerProvider(provider) {
|
|
43
|
+
if (!provider || typeof provider.id !== 'string' || !provider.id.trim() || typeof provider.collect !== 'function') {
|
|
44
|
+
throw new TypeError('Command palette providers need a non-empty id and collect function.')
|
|
45
|
+
}
|
|
46
|
+
const id = provider.id.trim()
|
|
47
|
+
if (providers.has(id)) throw new Error(`Command palette provider already registered: ${id}`)
|
|
48
|
+
providers.set(id, provider)
|
|
49
|
+
emit('provider-registered')
|
|
50
|
+
return () => {
|
|
51
|
+
if (providers.delete(id)) emit('provider-removed')
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
collect(context = {}) {
|
|
55
|
+
const commands = []
|
|
56
|
+
for (const [providerId, provider] of providers) {
|
|
57
|
+
const commandIds = new Set()
|
|
58
|
+
let provided
|
|
59
|
+
try { provided = provider.collect(context) }
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.warn(`[dsh-command-palette] provider ${providerId} failed:`, error)
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
for (const command of Array.isArray(provided) ? provided : []) {
|
|
65
|
+
const commandId = typeof command?.id === 'string' ? command.id.trim() : ''
|
|
66
|
+
const title = typeof command?.title === 'string' ? command.title.trim() : ''
|
|
67
|
+
if (!commandId || !title || typeof command.run !== 'function' || commandIds.has(commandId)) continue
|
|
68
|
+
commandIds.add(commandId)
|
|
69
|
+
const group = SUPPORTED_GROUPS.has(command.group) ? command.group : 'features'
|
|
70
|
+
commands.push({ ...command, group, title, id: `${providerId}:${commandId}` })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return commands
|
|
74
|
+
},
|
|
75
|
+
subscribe(listener) {
|
|
76
|
+
listeners.add(listener)
|
|
77
|
+
return () => listeners.delete(listener)
|
|
78
|
+
},
|
|
79
|
+
open() { if (!open) { open = true; emit('open') } },
|
|
80
|
+
close() { if (open) { open = false; emit('close') } },
|
|
81
|
+
toggle() { open = !open; emit('toggle') },
|
|
82
|
+
isOpen() { return open }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const css = `
|
|
86
|
+
.dshPaletteLayer{position:fixed;z-index:220;inset:0;padding:12vh 16px 24px;display:flex;align-items:flex-start;justify-content:center;background:rgba(4,7,12,.38);backdrop-filter:blur(7px);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--dsw-alias-label-primary)}
|
|
87
|
+
.dshPalettePanel{box-sizing:border-box;width:min(720px,100%);max-height:min(720px,80vh);padding-top:14px;overflow:hidden;display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);border-radius:18px;background:light-dark(rgba(250,250,250,.98),rgba(17,20,27,.98));box-shadow:0 28px 90px rgba(0,0,0,.42)}
|
|
88
|
+
.dshPaletteSearchWrap{box-sizing:border-box;min-height:70px;margin:0 14px;max-height:70px;padding:0 19px;display:flex;align-items:center;gap:13px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1,rgba(255,255,255,.035))}
|
|
89
|
+
.dshPaletteSearchIcon{color:var(--dsw-alias-label-tertiary);font-size:20px}.dshPaletteSearch,.dshPaletteSearch:focus,.dshPaletteSearch:focus-visible{min-width:0;flex:1;border:0!important;outline:0!important;box-shadow:none!important;-webkit-appearance:none;appearance:none;background:transparent;color:inherit;font:inherit;font-size:18px}.dshPaletteKey{padding:4px 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;color:var(--dsw-alias-label-tertiary);font-size:12px}
|
|
90
|
+
.dshPaletteResults{padding:8px;overflow:auto}.dshPaletteGroupTitle{margin:10px 10px 5px;color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:650;letter-spacing:.04em}.dshPaletteRow{box-sizing:border-box;width:100%;min-height:49px;padding:8px 10px;border:0;border-radius:10px;background:transparent;color:inherit;display:flex;align-items:center;gap:11px;text-align:left;font:inherit;cursor:pointer}.dshPaletteRow[data-selected]{background:var(--dsw-alias-interactive-bg-hover)}.dshPaletteRow:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.dshPaletteIcon{width:29px;height:29px;border-radius:8px;display:grid;place-items:center;background:var(--dsw-alias-bg-layer-2);font-size:14px}.dshPaletteBody{min-width:0;flex:1;display:flex;flex-direction:column;gap:2px}.dshPaletteTitle{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:600}.dshPaletteSubtitle{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:12px}.dshPaletteState{color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}.dshPaletteState[data-kind="approval"]{color:#f59e0b}.dshPaletteState[data-kind="running"]{color:#60a5fa}.dshPaletteState[data-kind="completed"]{color:#34d399}.dshPaletteCloseTab{padding:4px 7px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:11px;cursor:pointer}.dshPaletteCloseTab:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
|
|
91
|
+
.dshPaletteEmpty{padding:58px 24px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:13px}.dshPaletteError{margin:0 12px 10px;padding:9px 11px;border:1px solid rgba(239,68,68,.32);border-radius:9px;background:rgba(239,68,68,.09);color:#ef4444;font-size:12px}.dshPaletteFooter{padding:8px 14px;border-top:1px solid var(--dsw-alias-border-l2);display:flex;justify-content:flex-end;gap:12px;color:var(--dsw-alias-label-tertiary);font-size:11px}
|
|
92
|
+
.dshPaletteSettings{position:relative;z-index:1;isolation:isolate;box-sizing:border-box;max-width:680px;margin:0 auto;padding:28px 30px 32px;border:1px solid var(--dsw-alias-border-l2);border-radius:18px;background:light-dark(#f8fafc,#0b1220);box-shadow:0 12px 32px rgba(0,0,0,.16);color:var(--dsw-alias-label-primary)}.dshPaletteSettings h2{position:relative;margin:0 0 9px;font-size:20px;line-height:1.35}.dshPaletteSettings p{position:relative;margin:0 0 22px;color:var(--dsw-alias-label-secondary);font-size:14px;line-height:1.6}.dshPaletteSettingCard{position:relative;padding:18px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:light-dark(#fff,#0e1726);display:flex;flex-direction:column;gap:15px}.dshPaletteSettingLabel{font-size:12px;font-weight:650}.dshPaletteSettingInput{box-sizing:border-box;width:100%;height:38px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-base);color:inherit;font:inherit;font-size:13px;outline:none}.dshPaletteSettingInput:focus{border-color:var(--dsw-alias-brand-primary)}.dshPaletteSettingTextarea{box-sizing:border-box;min-height:90px;padding:10px 11px;resize:vertical;line-height:1.45}.dshPaletteSettingToggle{display:flex;align-items:center;gap:9px;font-size:13px}.dshPaletteSettingActions{display:flex;align-items:center;gap:10px}.dshPaletteSettingButton{height:34px;padding:0 14px;border:0;border-radius:8px;background:var(--dsw-alias-brand-primary,#4f46e5);color:#fff;font:inherit;font-size:13px;font-weight:600;cursor:pointer}.dshPaletteSettingButton[data-secondary]{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2)}.dshPaletteSettingStatus{font-size:12px;color:var(--dsw-alias-label-tertiary)}.dshPaletteSettingStatus[data-error]{color:#ef4444}.dshPaletteCustomList{display:flex;flex-direction:column;gap:8px}.dshPaletteCustomRow{display:flex;align-items:center;gap:10px;padding:10px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px}.dshPaletteCustomCopy{min-width:0;flex:1;display:flex;flex-direction:column;gap:3px}.dshPaletteCustomName{font-size:13px;font-weight:650}.dshPaletteCustomPrompt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:12px}.dshPaletteCustomTag{padding:3px 6px;border-radius:6px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:11px;white-space:nowrap}
|
|
93
|
+
@media (prefers-reduced-motion:no-preference){.dshPalettePanel{animation:dshPaletteIn .13s ease-out}@keyframes dshPaletteIn{from{opacity:0;transform:translateY(-8px) scale(.985)}to{opacity:1;transform:none}}}
|
|
94
|
+
`
|
|
95
|
+
|
|
96
|
+
function installStyles() {
|
|
97
|
+
if (document.querySelector('style[data-plugin-css="dsh-command-palette"]')) return
|
|
98
|
+
const tag = document.createElement('style')
|
|
99
|
+
tag.dataset.plugin = 'dsh-command-palette'
|
|
100
|
+
tag.dataset.pluginCss = 'dsh-command-palette'
|
|
101
|
+
tag.textContent = css
|
|
102
|
+
document.head.appendChild(tag)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function searchable(command) {
|
|
106
|
+
return [command.title, command.subtitle, ...(command.keywords || [])].filter(Boolean).join(' ').toLocaleLowerCase()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function recentTimestamp(value) {
|
|
110
|
+
return typeof value === 'number' ? value : Date.parse(String(value || '')) || 0
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function commandState(item) {
|
|
114
|
+
if (item.pendingInteraction) return { text: item.pendingInteraction === 'approval' ? '等待审批' : '等待操作', kind: 'approval' }
|
|
115
|
+
if (item.running) return { text: '运行中', kind: 'running' }
|
|
116
|
+
if (item.completed) return { text: '已完成', kind: 'completed' }
|
|
117
|
+
if (item.current) return { text: '当前', kind: 'current' }
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function groupVisibleCommands(commands, hasQuery) {
|
|
122
|
+
const byGroup = (group) => commands.filter((item) => item.group === group)
|
|
123
|
+
if (hasQuery) return GROUP_ORDER.map((group) => [group, byGroup(group)]).filter(([, items]) => items.length)
|
|
124
|
+
const recentSessions = byGroup('sessions')
|
|
125
|
+
.sort((left, right) => recentTimestamp(right.updatedAt) - recentTimestamp(left.updatedAt))
|
|
126
|
+
.slice(0, 6)
|
|
127
|
+
const otherGroups = GROUP_ORDER
|
|
128
|
+
.filter((group) => group !== 'sessions')
|
|
129
|
+
.map((group) => [group, byGroup(group)])
|
|
130
|
+
.filter(([, items]) => items.length)
|
|
131
|
+
return recentSessions.length ? [['recent', recentSessions], ...otherGroups] : otherGroups
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function buildCommands({ sessions, workspaces, customCommands, extensionCommands, runCustomCommand, openSession, startSession }) {
|
|
135
|
+
const commands = []
|
|
136
|
+
for (const command of customCommands) {
|
|
137
|
+
commands.push({
|
|
138
|
+
id: `custom:${command.id}`, group: 'features', icon: '⌘', title: command.name,
|
|
139
|
+
subtitle: command.sticky ? '自定义指令 · 复用专属会话' : '自定义指令 · 新建会话',
|
|
140
|
+
keywords: ['custom', '自定义指令', command.prompt],
|
|
141
|
+
run: () => runCustomCommand(command)
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
commands.push(...extensionCommands)
|
|
145
|
+
for (const workspace of workspaces?.items || []) {
|
|
146
|
+
const workspaceId = workspace.workspaceId || workspace.id
|
|
147
|
+
if (!workspaceId) continue
|
|
148
|
+
commands.push({
|
|
149
|
+
id: `workspace:${workspaceId}`, group: 'workspaces', icon: '◇', title: workspace.title || workspace.path || '工作区', subtitle: workspace.path || '', keywords: [workspaceId, workspace.path],
|
|
150
|
+
run: () => startSession(workspaceId)
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
const archived = new Set(workspaces?.archivedSessionIds || [])
|
|
154
|
+
for (const id of sessions?.ids || []) {
|
|
155
|
+
const item = sessions.byId?.[id]
|
|
156
|
+
if (!item || item.blank || item.archived === true || item.origin === 'subagent' || archived.has(id)) continue
|
|
157
|
+
commands.push({
|
|
158
|
+
id: `session:${id}`, group: 'sessions', icon: '◆', title: item.displayTitle || item.title || id, subtitle: item.cwd || '',
|
|
159
|
+
keywords: [item.agentPreset, item.cwd, id], updatedAt: item.updatedAt, current: id === sessions.current, running: item.running, pendingInteraction: item.pendingInteraction, completed: item.completed,
|
|
160
|
+
run: () => openSession(id)
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
return commands
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function Palette({ useSessions, useWorkspaces, useCommandSettings, saveCustomCommands, openSession, startSession, runCustomCommand, paletteService }) {
|
|
167
|
+
const sessions = useSessions((value) => value)
|
|
168
|
+
const workspaces = useWorkspaces((value) => value)
|
|
169
|
+
const commandSettings = useCommandSettings((value) => value)
|
|
170
|
+
const customCommands = normalizeCustomCommands(commandSettings.value?.commands)
|
|
171
|
+
const [open, setOpen] = React.useState(paletteService.isOpen())
|
|
172
|
+
const [query, setQuery] = React.useState('')
|
|
173
|
+
const [selected, setSelected] = React.useState(0)
|
|
174
|
+
const [serviceRevision, setServiceRevision] = React.useState(0)
|
|
175
|
+
const [executing, setExecuting] = React.useState(false)
|
|
176
|
+
const [error, setError] = React.useState('')
|
|
177
|
+
const previousFocus = React.useRef(null)
|
|
178
|
+
const paletteOpen = React.useRef(false)
|
|
179
|
+
const lastShiftDownAt = React.useRef(null)
|
|
180
|
+
const extensionCommands = React.useMemo(() => paletteService.collect({ sessions, workspaces }), [paletteService, sessions, workspaces, serviceRevision])
|
|
181
|
+
const commands = React.useMemo(() => buildCommands({ sessions, workspaces, customCommands, extensionCommands, runCustomCommand, openSession, startSession }), [sessions, workspaces, customCommands, extensionCommands, runCustomCommand, openSession, startSession])
|
|
182
|
+
const normalizedQuery = query.trim().toLocaleLowerCase()
|
|
183
|
+
const filtered = React.useMemo(() => commands.filter((item) => !normalizedQuery || searchable(item).includes(normalizedQuery)), [commands, normalizedQuery])
|
|
184
|
+
const grouped = React.useMemo(() => groupVisibleCommands(filtered, Boolean(normalizedQuery)), [filtered, normalizedQuery])
|
|
185
|
+
const visibleCommands = React.useMemo(() => grouped.flatMap(([, items]) => items), [grouped])
|
|
186
|
+
|
|
187
|
+
const close = React.useCallback((restoreFocus = true) => {
|
|
188
|
+
paletteOpen.current = false
|
|
189
|
+
setOpen(false); setQuery(''); setSelected(0); setError(''); setExecuting(false)
|
|
190
|
+
paletteService.close()
|
|
191
|
+
if (restoreFocus) requestAnimationFrame(() => previousFocus.current?.isConnected && previousFocus.current.focus?.())
|
|
192
|
+
}, [paletteService])
|
|
193
|
+
|
|
194
|
+
const showPalette = React.useCallback(() => {
|
|
195
|
+
if (paletteOpen.current) return
|
|
196
|
+
previousFocus.current = document.activeElement
|
|
197
|
+
paletteOpen.current = true
|
|
198
|
+
setOpen(true); setQuery(''); setSelected(0); setError('')
|
|
199
|
+
paletteService.open()
|
|
200
|
+
}, [paletteService])
|
|
201
|
+
|
|
202
|
+
React.useEffect(() => {
|
|
203
|
+
const unsubscribe = paletteService.subscribe((snapshot) => {
|
|
204
|
+
setServiceRevision((value) => value + 1)
|
|
205
|
+
if (snapshot.open) showPalette()
|
|
206
|
+
else if (paletteOpen.current) {
|
|
207
|
+
paletteOpen.current = false
|
|
208
|
+
setOpen(false); setQuery(''); setSelected(0); setError(''); setExecuting(false)
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
const onFocusedWindowKeyDown = (event) => {
|
|
212
|
+
if (event.isComposing) return
|
|
213
|
+
if (event.key !== 'Shift' || event.repeat || event.altKey || event.ctrlKey || event.metaKey) {
|
|
214
|
+
lastShiftDownAt.current = null
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
const now = performance.now()
|
|
218
|
+
if (lastShiftDownAt.current !== null && now - lastShiftDownAt.current <= 400) {
|
|
219
|
+
lastShiftDownAt.current = null
|
|
220
|
+
paletteService.toggle()
|
|
221
|
+
} else {
|
|
222
|
+
lastShiftDownAt.current = now
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
window.addEventListener('keydown', onFocusedWindowKeyDown, true)
|
|
226
|
+
return () => {
|
|
227
|
+
unsubscribe?.()
|
|
228
|
+
window.removeEventListener('keydown', onFocusedWindowKeyDown, true)
|
|
229
|
+
}
|
|
230
|
+
}, [paletteService, showPalette])
|
|
231
|
+
|
|
232
|
+
React.useEffect(() => { paletteOpen.current = open }, [open])
|
|
233
|
+
|
|
234
|
+
React.useEffect(() => {
|
|
235
|
+
if (selected >= visibleCommands.length) setSelected(Math.max(0, visibleCommands.length - 1))
|
|
236
|
+
}, [visibleCommands.length, selected])
|
|
237
|
+
|
|
238
|
+
const execute = React.useCallback(async (command) => {
|
|
239
|
+
if (!command || executing) return
|
|
240
|
+
setExecuting(true); setError('')
|
|
241
|
+
try { await command.run(); close(false) }
|
|
242
|
+
catch (cause) { setExecuting(false); setError(String(cause?.message || cause || '命令执行失败')) }
|
|
243
|
+
}, [executing, close])
|
|
244
|
+
|
|
245
|
+
if (!open) return null
|
|
246
|
+
const activeId = visibleCommands[selected] ? `dsh-palette-option-${selected}` : undefined
|
|
247
|
+
const onKeyDown = (event) => {
|
|
248
|
+
if (event.isComposing) return
|
|
249
|
+
if (event.key === 'Escape') { event.preventDefault(); close(true); return }
|
|
250
|
+
if (event.key === 'ArrowDown') { event.preventDefault(); setSelected((value) => Math.min(value + 1, Math.max(0, visibleCommands.length - 1))); return }
|
|
251
|
+
if (event.key === 'ArrowUp') { event.preventDefault(); setSelected((value) => Math.max(0, value - 1)); return }
|
|
252
|
+
if (event.key === 'Enter') { event.preventDefault(); void execute(visibleCommands[selected]) }
|
|
253
|
+
}
|
|
254
|
+
let flatIndex = -1
|
|
255
|
+
return ReactDOM.createPortal(React.createElement('div', { className: 'dshPaletteLayer', onMouseDown: (event) => { if (event.target === event.currentTarget) close(true) } },
|
|
256
|
+
React.createElement('section', { className: 'dshPalettePanel', role: 'dialog', 'aria-modal': 'true', 'aria-label': '命令面板' },
|
|
257
|
+
React.createElement('div', { className: 'dshPaletteSearchWrap' },
|
|
258
|
+
React.createElement('span', { className: 'dshPaletteSearchIcon', 'aria-hidden': 'true' }, '⌕'),
|
|
259
|
+
React.createElement('input', { autoFocus: true, className: 'dshPaletteSearch', role: 'combobox', 'aria-expanded': 'true', 'aria-controls': 'dsh-palette-results', 'aria-activedescendant': activeId, placeholder: '搜索功能、工作区或会话…', value: query, onChange: (event) => { setQuery(event.target.value); setSelected(0) }, onKeyDown }),
|
|
260
|
+
React.createElement('span', { className: 'dshPaletteKey' }, 'Esc')
|
|
261
|
+
),
|
|
262
|
+
React.createElement('div', { id: 'dsh-palette-results', className: 'dshPaletteResults', role: 'listbox' },
|
|
263
|
+
grouped.length === 0 ? React.createElement('div', { className: 'dshPaletteEmpty' }, '没有找到匹配内容') : grouped.map(([group, items]) =>
|
|
264
|
+
React.createElement(React.Fragment, { key: group },
|
|
265
|
+
React.createElement('div', { className: 'dshPaletteGroupTitle' }, GROUP_LABELS[group]),
|
|
266
|
+
items.map((item) => {
|
|
267
|
+
flatIndex += 1; const index = flatIndex; const state = commandState(item)
|
|
268
|
+
return React.createElement('div', { key: item.id, style: { display: 'flex', alignItems: 'center' } },
|
|
269
|
+
React.createElement('button', { id: `dsh-palette-option-${index}`, type: 'button', className: 'dshPaletteRow', role: 'option', 'aria-selected': index === selected, 'data-selected': index === selected ? '' : undefined, onMouseEnter: () => setSelected(index), onClick: () => void execute(item) },
|
|
270
|
+
React.createElement('span', { className: 'dshPaletteIcon', 'aria-hidden': 'true' }, item.icon),
|
|
271
|
+
React.createElement('span', { className: 'dshPaletteBody' }, React.createElement('span', { className: 'dshPaletteTitle' }, item.title), item.subtitle ? React.createElement('span', { className: 'dshPaletteSubtitle' }, item.subtitle) : null),
|
|
272
|
+
state ? React.createElement('span', { className: 'dshPaletteState', 'data-kind': state.kind }, state.text) : null
|
|
273
|
+
),
|
|
274
|
+
item.close ? React.createElement('button', { type: 'button', className: 'dshPaletteCloseTab', title: '执行次要操作', 'aria-label': `操作${item.title}`, onClick: async (event) => { event.stopPropagation(); try { await item.close(); setServiceRevision((value) => value + 1) } catch (cause) { setError(String(cause?.message || cause)) } } }, '操作') : null
|
|
275
|
+
)
|
|
276
|
+
})
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
),
|
|
280
|
+
error ? React.createElement('div', { className: 'dshPaletteError', role: 'alert' }, error) : null,
|
|
281
|
+
React.createElement('footer', { className: 'dshPaletteFooter' }, React.createElement('span', null, '↑↓ 选择'), React.createElement('span', null, 'Enter 打开'))
|
|
282
|
+
)
|
|
283
|
+
), document.body)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function ShortcutSettings({ useWorkspaces, useCommandSettings, saveCustomCommands }) {
|
|
287
|
+
const commandSettings = useCommandSettings((value) => value)
|
|
288
|
+
const commands = normalizeCustomCommands(commandSettings.value?.commands)
|
|
289
|
+
const workspaceState = typeof useWorkspaces === 'function' ? useWorkspaces((value) => value) : { items: [] }
|
|
290
|
+
const workspaces = workspaceState?.items || []
|
|
291
|
+
const emptyDraft = { name: '', prompt: '', sticky: true, workspaceId: '' }
|
|
292
|
+
const [draft, setDraft] = React.useState(emptyDraft)
|
|
293
|
+
const [editingId, setEditingId] = React.useState(null)
|
|
294
|
+
const [saveError, setSaveError] = React.useState('')
|
|
295
|
+
const save = async () => {
|
|
296
|
+
const next = normalizeCustomCommand({ ...draft, id: editingId || undefined })
|
|
297
|
+
if (!next) return
|
|
298
|
+
try {
|
|
299
|
+
if (editingId) {
|
|
300
|
+
const previous = commands.find((command) => command.id === editingId)
|
|
301
|
+
next.stickySessionId = previous?.sticky && next.sticky ? previous.stickySessionId : undefined
|
|
302
|
+
await saveCustomCommands(commands.map((command) => command.id === editingId ? next : command))
|
|
303
|
+
} else {
|
|
304
|
+
await saveCustomCommands([...commands, next])
|
|
305
|
+
}
|
|
306
|
+
setSaveError(''); setDraft(emptyDraft); setEditingId(null)
|
|
307
|
+
} catch (cause) {
|
|
308
|
+
setSaveError(String(cause?.message || cause))
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const remove = async (id) => {
|
|
312
|
+
try { await saveCustomCommands(commands.filter((command) => command.id !== id)); setSaveError('') }
|
|
313
|
+
catch (cause) { setSaveError(String(cause?.message || cause)) }
|
|
314
|
+
}
|
|
315
|
+
const edit = (command) => {
|
|
316
|
+
setEditingId(command.id)
|
|
317
|
+
setDraft({ name: command.name, prompt: command.prompt, sticky: command.sticky, workspaceId: command.workspaceId || '' })
|
|
318
|
+
}
|
|
319
|
+
return React.createElement('div', { className: 'dshPaletteSettings' },
|
|
320
|
+
React.createElement('h2', null, '快捷键'),
|
|
321
|
+
React.createElement('p', null, '命令面板只在当前 DSH Web 窗口获得焦点时响应。'),
|
|
322
|
+
React.createElement('div', { className: 'dshPaletteSettingCard' },
|
|
323
|
+
React.createElement('div', { className: 'dshPaletteSettingLabel' }, '唤起命令面板'),
|
|
324
|
+
React.createElement('div', { className: 'dshPaletteSettingInput', role: 'status' }, '连续快速按两次 Shift'),
|
|
325
|
+
React.createElement('span', { className: 'dshPaletteSettingStatus' }, '两次按键间隔需不超过 400 毫秒;不会监听其他应用。')
|
|
326
|
+
),
|
|
327
|
+
React.createElement('h2', { style: { marginTop: 28 } }, '自定义指令'),
|
|
328
|
+
React.createElement('p', null, '为常用任务配置名称和发送给 DSH 的内容。启用“复用专属会话”后,同一指令会持续发送到它首次创建的会话。'),
|
|
329
|
+
React.createElement('div', { className: 'dshPaletteSettingCard' },
|
|
330
|
+
React.createElement('label', null, React.createElement('div', { className: 'dshPaletteSettingLabel' }, '指令名称'), React.createElement('input', { className: 'dshPaletteSettingInput', value: draft.name, maxLength: 80, placeholder: '例如:生成每日新闻', onChange: (event) => setDraft({ ...draft, name: event.target.value }) })),
|
|
331
|
+
React.createElement('label', null, React.createElement('div', { className: 'dshPaletteSettingLabel' }, '发送给 DSH 的指令内容'), React.createElement('textarea', { className: 'dshPaletteSettingInput dshPaletteSettingTextarea', value: draft.prompt, maxLength: 12000, placeholder: '例如:生成今日的每日新闻,并保存日报。', onChange: (event) => setDraft({ ...draft, prompt: event.target.value }) })),
|
|
332
|
+
React.createElement('label', null, React.createElement('div', { className: 'dshPaletteSettingLabel' }, '新会话的目标工作区'), React.createElement('select', { className: 'dshPaletteSettingInput', value: draft.workspaceId, onChange: (event) => setDraft({ ...draft, workspaceId: event.target.value }) },
|
|
333
|
+
React.createElement('option', { value: '' }, '未分组(默认)'),
|
|
334
|
+
workspaces.map((workspace) => {
|
|
335
|
+
const workspaceId = workspace.workspaceId || workspace.id
|
|
336
|
+
return workspaceId ? React.createElement('option', { key: workspaceId, value: workspaceId }, workspace.title || workspace.path || workspaceId) : null
|
|
337
|
+
})
|
|
338
|
+
)),
|
|
339
|
+
React.createElement('span', { className: 'dshPaletteSettingStatus' }, '只在该指令创建新会话时生效;已有粘性会话不会被移动。'),
|
|
340
|
+
React.createElement('label', { className: 'dshPaletteSettingToggle' }, React.createElement('input', { type: 'checkbox', checked: draft.sticky, onChange: (event) => setDraft({ ...draft, sticky: event.target.checked }) }), React.createElement('span', null, '复用专属会话(推荐用于日报、周报等周期任务)')),
|
|
341
|
+
React.createElement('div', { className: 'dshPaletteSettingActions' },
|
|
342
|
+
React.createElement('button', { type: 'button', className: 'dshPaletteSettingButton', disabled: !draft.name.trim() || !draft.prompt.trim(), onClick: () => void save() }, editingId ? '保存修改' : '添加指令'),
|
|
343
|
+
editingId ? React.createElement('button', { type: 'button', className: 'dshPaletteSettingButton', 'data-secondary': '', onClick: () => { setDraft(emptyDraft); setEditingId(null) } }, '取消') : null
|
|
344
|
+
),
|
|
345
|
+
saveError ? React.createElement('span', { className: 'dshPaletteSettingStatus', 'data-error': '' }, `保存失败:${saveError}`) : null,
|
|
346
|
+
commands.length ? React.createElement('div', { className: 'dshPaletteCustomList' }, commands.map((command) => React.createElement('div', { className: 'dshPaletteCustomRow', key: command.id },
|
|
347
|
+
React.createElement('div', { className: 'dshPaletteCustomCopy' }, React.createElement('span', { className: 'dshPaletteCustomName' }, command.name), React.createElement('span', { className: 'dshPaletteCustomPrompt' }, command.prompt)),
|
|
348
|
+
React.createElement('span', { className: 'dshPaletteCustomTag' }, command.sticky ? command.stickySessionId ? '已绑定会话' : '首次创建后复用' : '每次新建'),
|
|
349
|
+
React.createElement('button', { type: 'button', className: 'dshPaletteSettingButton', 'data-secondary': '', onClick: () => edit(command) }, '编辑'),
|
|
350
|
+
React.createElement('button', { type: 'button', className: 'dshPaletteSettingButton', 'data-secondary': '', onClick: () => void remove(command.id) }, '删除')
|
|
351
|
+
))) : React.createElement('span', { className: 'dshPaletteSettingStatus' }, '尚未配置自定义指令。')
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const inject = ['slots', 'sessions', 'workspaces', 'settingsScope']
|
|
357
|
+
function apply(ctx) {
|
|
358
|
+
const commandSettings = ctx.get('settingsScope').bind({ namespace: 'command-palette' })
|
|
359
|
+
const paletteService = createCommandPaletteService()
|
|
360
|
+
ctx.provide('commandPalette', paletteService)
|
|
361
|
+
installStyles()
|
|
362
|
+
const saveCustomCommands = async (next) => {
|
|
363
|
+
const commands = normalizeCustomCommands(next)
|
|
364
|
+
await commandSettings.set('commands', commands)
|
|
365
|
+
return commands
|
|
366
|
+
}
|
|
367
|
+
const runCustomCommand = async (command) => {
|
|
368
|
+
const snapshot = ctx.sessions.list.getSnapshot()
|
|
369
|
+
const archived = new Set(ctx.workspaces.list?.getSnapshot?.().archivedSessionIds || [])
|
|
370
|
+
let sessionId = command.sticky ? command.stickySessionId : undefined
|
|
371
|
+
const stickySession = sessionId ? snapshot.byId?.[sessionId] : undefined
|
|
372
|
+
if (!stickySession || stickySession.archived === true || stickySession.origin === 'subagent' || archived.has(sessionId)) {
|
|
373
|
+
const configuredWorkspace = command.workspaceId && ctx.workspaces.list.getSnapshot().items.some((workspace) => (workspace.workspaceId || workspace.id) === command.workspaceId)
|
|
374
|
+
? command.workspaceId
|
|
375
|
+
: undefined
|
|
376
|
+
sessionId = configuredWorkspace ? await ctx.sessions.create({ workspaceId: configuredWorkspace }) : await ctx.sessions.create()
|
|
377
|
+
if (command.sticky) {
|
|
378
|
+
const current = normalizeCustomCommands(commandSettings.getSnapshot().value?.commands)
|
|
379
|
+
await saveCustomCommands(current.map((item) => item.id === command.id ? { ...item, stickySessionId: sessionId } : item))
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
ctx.sessions.open(sessionId)
|
|
383
|
+
const sessionContext = ctx.sessions.scope(sessionId)
|
|
384
|
+
const session = sessionContext ? ctx.sessions.sessionOf(sessionContext) : undefined
|
|
385
|
+
if (!session) throw new Error('当前 DSH 版本无法定位自定义指令的目标会话。')
|
|
386
|
+
const result = await session.prompt([{ type: 'text', text: command.prompt }], 'queue')
|
|
387
|
+
if (!result.ok) throw new Error(`自定义指令发送失败:${result.error?.message || '未知错误'}`)
|
|
388
|
+
}
|
|
389
|
+
const injected = () => ({
|
|
390
|
+
useSessions: ctx.sessions.list,
|
|
391
|
+
useWorkspaces: ctx.workspaces.list,
|
|
392
|
+
useCommandSettings: commandSettings,
|
|
393
|
+
saveCustomCommands,
|
|
394
|
+
paletteService,
|
|
395
|
+
openSession: (sessionId) => ctx.sessions.open(sessionId),
|
|
396
|
+
startSession: (workspaceId) => ctx.workspaces.startSession(workspaceId),
|
|
397
|
+
runCustomCommand
|
|
398
|
+
})
|
|
399
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register({ name: 'shell.overlay', id: 'dsh-command-palette', order: 220, inject: injected }, Palette))
|
|
400
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'command-palette-settings', order: 90, label: () => '命令面板', inject: injected }, ShortcutSettings))
|
|
401
|
+
ctx.effect(() => () => {
|
|
402
|
+
paletteService.close()
|
|
403
|
+
document.querySelector('style[data-plugin-css="dsh-command-palette"]')?.remove()
|
|
404
|
+
}, 'dsh-command-palette: cleanup')
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
exports.apply = apply
|
|
408
|
+
exports.inject = inject
|
|
409
|
+
exports.__testing = { groupVisibleCommands }
|
|
410
|
+
return module.exports
|
|
411
|
+
}
|
|
412
|
+
})
|
package/cordis.patch.yml
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
2
|
+
|
|
3
|
+
export const name = 'command-palette'
|
|
4
|
+
export const inject = ['settings']
|
|
5
|
+
export const SETTINGS_NAMESPACE = 'command-palette'
|
|
6
|
+
|
|
7
|
+
const CustomCommand = Schema.object({
|
|
8
|
+
id: Schema.string().required().description('Stable command identifier'),
|
|
9
|
+
name: Schema.string().required().description('Display name'),
|
|
10
|
+
prompt: Schema.string().required().description('Prompt sent to DSH'),
|
|
11
|
+
sticky: Schema.boolean().default(true).description('Reuse a dedicated session'),
|
|
12
|
+
workspaceId: Schema.string().default('').description('Workspace used for new sessions'),
|
|
13
|
+
stickySessionId: Schema.string().default('').description('Session currently bound to this command')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const Config = Schema.object({
|
|
17
|
+
commands: Schema.array(CustomCommand).default([]).description('Reusable custom commands')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export function apply(ctx, config) {
|
|
21
|
+
ctx.inject(['settings'], (settingsCtx) => {
|
|
22
|
+
settingsCtx.settings.register(SETTINGS_NAMESPACE, Config, { base: config || {} })
|
|
23
|
+
})
|
|
24
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
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.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "chenyangcun",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./index.js",
|
|
11
|
+
"./client": "./client.js",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"index.js",
|
|
16
|
+
"client.js",
|
|
17
|
+
"cordis.patch.yml",
|
|
18
|
+
"README.md",
|
|
19
|
+
"README.en.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "node --test test/*.test.mjs",
|
|
24
|
+
"check": "node --check index.js && node --check client.js && npm test"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/chenyangcun/dsh-command-palette.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/chenyangcun/dsh-command-palette#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/chenyangcun/dsh-command-palette/issues"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"dsh",
|
|
36
|
+
"dsh-plugin",
|
|
37
|
+
"deepseek-harness",
|
|
38
|
+
"command-palette",
|
|
39
|
+
"workspace",
|
|
40
|
+
"session"
|
|
41
|
+
],
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@deepseek-ai/schemastery": "3.18.1"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"jsdom": "26.1.0",
|
|
50
|
+
"react": "18.3.1",
|
|
51
|
+
"react-dom": "18.3.1"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@deepseek-ai/dsh-client-runtime": ">=0.1.1-rc.2 <0.2.0-0",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-layout": ">=0.1.1-rc.2 <0.2.0-0",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.1-rc.2 <0.2.0-0",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-sidebar": ">=0.1.1-rc.2 <0.2.0-0",
|
|
58
|
+
"@deepseek-ai/dsh-settings": ">=0.1.1-rc.2 <0.2.0-0",
|
|
59
|
+
"react": ">=18.2.0 <19.0.0-0",
|
|
60
|
+
"react-dom": ">=18.2.0 <19.0.0-0"
|
|
61
|
+
},
|
|
62
|
+
"dsh": {
|
|
63
|
+
"bundle": {
|
|
64
|
+
"patch": "./cordis.patch.yml"
|
|
65
|
+
},
|
|
66
|
+
"client": {
|
|
67
|
+
"platform": "web",
|
|
68
|
+
"inject": [
|
|
69
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
71
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
72
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|