dsh-vscode-mode 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -1
- package/lib/client.js +91 -13
- package/lib/client.js.map +1 -1
- package/lib/index.js +465 -2
- package/lib/index.js.map +1 -1
- package/package.json +2 -1
- package/skills/dsh-vscodemode-mcp/SKILL.md +181 -0
- package/src/client/styles/editor.css +2 -0
- package/src/client/ui/QuickOpen.ts +61 -13
- package/src/client/ui/searchNav.ts +24 -0
- package/src/compat.ts +4 -0
- package/src/index.ts +5 -1
- package/src/paths.ts +11 -0
- package/src/skills.ts +534 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"files": [
|
|
36
36
|
"lib",
|
|
37
37
|
"assets",
|
|
38
|
+
"skills",
|
|
38
39
|
"src",
|
|
39
40
|
"unity",
|
|
40
41
|
"cordis.patch.yml"
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dsh-vscodemode-mcp
|
|
3
|
+
description: 配置、新增、启用或排查 dsh-vscode-mode 插件(VSCodeMode 设置页)的 MCP 服务——全局「我的 MCP」与项目级 `.mcp.json` 两种作用域、stdio / streamable-http 传输写法、`mcp.*` RPC 方法、serverName 命名与冲突规则、项目 MCP 的工作区隔离边界,以及连接失败/工具数为 0 等故障的定位路径。当用户提到「加个 MCP」「配置 MCP」「MCP 连不上」「项目 MCP」「.mcp.json」「VSCodeMode 的 MCP 管理」时加载本技能。
|
|
4
|
+
whenToUse: 用户要求新增/修改/启停/排查 dsh-vscode-mode 的 MCP 服务配置时;或需要判断某个 MCP 工具为何在对话中不可见/被拒绝时。
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# dsh-vscodemode-mcp — dsh-vscode-mode 的 MCP 配置与使用
|
|
8
|
+
|
|
9
|
+
本技能覆盖 **dsh-vscode-mode 插件**(DSH 上的类 VSCode 编码体验)提供的 MCP 可视化管理能力。
|
|
10
|
+
它管理的是 `@deepseek-ai/dsh-mcp-client` 的 loader 条目,连接由 DSH Host 维护,**不在浏览器侧连接 MCP**。
|
|
11
|
+
|
|
12
|
+
## 1. 两种作用域(先判断用户要哪一种)
|
|
13
|
+
|
|
14
|
+
| | 全局「我的 MCP」 | 项目 MCP |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| 配置位置 | profile 的 loader 树(`~/.dsh/profiles/<profile>/cordis.yml`) | 项目根 `.mcp.json` 的 `mcpServers` |
|
|
17
|
+
| 生效范围 | 该 profile 下所有工作区 | 仅该 workspace(+ 其子路径可匹配到的工作区) |
|
|
18
|
+
| 随仓库共享 | 否 | 是(`.mcp.json` 入库即团队共享) |
|
|
19
|
+
| 持久真相 | loader 条目 | `.mcp.json` 文件(重启后由插件 reconcile 恢复激活) |
|
|
20
|
+
| 受工作区隔离约束 | 否 | **是**(见 §5) |
|
|
21
|
+
|
|
22
|
+
判断规则:
|
|
23
|
+
- 用户说「全局都能用」「所有项目都要」→ 全局。
|
|
24
|
+
- 用户说「这个项目需要」「跟仓库一起提交」→ 项目级。
|
|
25
|
+
- 不确定且该 MCP 只在某个仓库有意义 → 默认项目级。
|
|
26
|
+
|
|
27
|
+
## 2. UI 路径
|
|
28
|
+
|
|
29
|
+
设置 → **VSCodeMode** → **「MCP 管理」** Tab,其下三个子页签:
|
|
30
|
+
|
|
31
|
+
- **我的 MCP** — 全局服务列表(可查看状态/工具、添加、刷新 ⟳、启用/禁用、删除 ⌫)。
|
|
32
|
+
- **项目 MCP** — 先选项目(带路径与 MCP 计数),再管理该项目的服务。
|
|
33
|
+
- **MCP 市场** — 占位,暂未接入。
|
|
34
|
+
|
|
35
|
+
「+ 添加全局 MCP」按钮在子页签栏右侧;项目 MCP 的新增入口在所选项目的分组内。
|
|
36
|
+
表单字段:名称 / 传输方式 / (stdio)命令·参数·工作目录 / (streamable-http)URL·请求头。
|
|
37
|
+
按钮文案为「保存并连接」——**保存后会立刻尝试连接**。
|
|
38
|
+
|
|
39
|
+
## 3. 配置写法
|
|
40
|
+
|
|
41
|
+
### 3.1 项目级 `.mcp.json`
|
|
42
|
+
|
|
43
|
+
对齐 Claude Code / Cursor 格式,存于项目根:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"codegraph": {
|
|
49
|
+
"command": "codegraph",
|
|
50
|
+
"args": ["serve", "--mcp", "--path", "D:/Work/MyProject"]
|
|
51
|
+
},
|
|
52
|
+
"remote-tools": {
|
|
53
|
+
"url": "http://localhost:3000/mcp",
|
|
54
|
+
"headers": { "Authorization": "Bearer <token>" },
|
|
55
|
+
"toolCallTimeoutMs": 60000
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
规则(`src/mcpProject.ts` 的 `configFromDef`):
|
|
62
|
+
- **有 `url` → `streamable-http`;无 `url` → `stdio`**。传输方式由字段形态自动判定,不写 `transport`。
|
|
63
|
+
- stdio 字段:`command`(必填)、`args`(数组)、`cwd`、`env`(键值对象)。
|
|
64
|
+
- http 字段:`url`(必填,须 `http(s)://`)、`headers`(键值对象)。
|
|
65
|
+
- 可选:`toolCallTimeoutMs`(数字)。
|
|
66
|
+
- `disabled: true` = 停用(由启停开关维护;**保存时不会写这个键,保存恒为启用**)。
|
|
67
|
+
- 顶层可有其他字段,写回时保留;`mcpServers` 之外的未知字段不丢。
|
|
68
|
+
|
|
69
|
+
### 3.2 全局(等价配置形)
|
|
70
|
+
|
|
71
|
+
全局条目等价于 profile loader 里的一行 `@deepseek-ai/dsh-mcp-client`:
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
- id: <条目 id>
|
|
75
|
+
name: '@deepseek-ai/dsh-mcp-client'
|
|
76
|
+
config:
|
|
77
|
+
serverName: codegraph
|
|
78
|
+
transport: stdio
|
|
79
|
+
command: codegraph
|
|
80
|
+
args: ['serve', '--mcp']
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
但 **优先用设置页 UI 或 `mcp.save` RPC**,不要手改 profile 的 `cordis.yml`:UI/RPC 会同步触发 loader 热更新与冲突校验,手改容易产生重复条目。
|
|
84
|
+
|
|
85
|
+
### 3.3 两个最小可用示例
|
|
86
|
+
|
|
87
|
+
stdio(本地命令行 MCP,如 CodeGraph):
|
|
88
|
+
```
|
|
89
|
+
名称: codegraph
|
|
90
|
+
传输方式: stdio
|
|
91
|
+
命令: codegraph
|
|
92
|
+
参数: serve --mcp --path D:/Work/MyProject
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
streamable-http(远端/已启动的 HTTP MCP):
|
|
96
|
+
```
|
|
97
|
+
名称: remote-tools
|
|
98
|
+
传输方式: streamable-http
|
|
99
|
+
URL: http://localhost:3000/mcp
|
|
100
|
+
请求头: Authorization=Bearer <token>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 4. RPC 方法表(UI 之外的程序化入口)
|
|
104
|
+
|
|
105
|
+
路由:`POST /edrv/rpc`,报文体 `{ "method": "...", "args": { ... } }`,返回 `{ ok: true, ... }` 或 `{ ok: false, error }`。
|
|
106
|
+
|
|
107
|
+
| 方法 | 入参 | 返回 |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `mcp.list` | `{}` | `{ servers: MpcServer[] }` |
|
|
110
|
+
| `mcp.save` | `{ config: MpcConfig }` | `{ server: MpcServer }` |
|
|
111
|
+
| `mcp.remove` | `{ id }` | `{}` |
|
|
112
|
+
| `mcp.toggle` | `{ id, enabled }` | `{ server: MpcServer }` |
|
|
113
|
+
| `mcp.refresh` | `{ id }` | `{ server: MpcServer }` |
|
|
114
|
+
| `mcp.projects` | `{}` | `{ projects: MpcProject[] }` |
|
|
115
|
+
| `mcp.projectSave` | `{ workspacePath, serverName, config }` | `{ project: MpcProject }` |
|
|
116
|
+
| `mcp.projectRemove` | `{ workspacePath, serverName }` | `{ project: MpcProject }` |
|
|
117
|
+
| `mcp.projectToggle` | `{ workspacePath, serverName, enabled }` | `{ project: MpcProject }` |
|
|
118
|
+
| `mcp.projectRefresh` | `{ workspacePath, serverName }` | `{ project: MpcProject }` |
|
|
119
|
+
|
|
120
|
+
`MpcServer` 关键字段:`id`、`serverName`、`enabled`、`transport`、`config`、`status`、`toolCount`、`tools[]`。
|
|
121
|
+
`MpcProject` 关键字段:`workspacePath`、`title`、`servers[]`、`source: 'project'`,异常时带 `missingDir` 或 `fileError`。
|
|
122
|
+
|
|
123
|
+
## 5. 命名规则与冲突(最容易踩的坑)
|
|
124
|
+
|
|
125
|
+
- **`serverName` 规则**:`/^[A-Za-z0-9_-]{1,32}$/` —— 字母、数字、**下划线**、连字符,最多 32 位。
|
|
126
|
+
⚠️ 注意与技能名的区别:**MCP 的 `serverName` 允许下划线**,而 DSH 技能名不允许(技能名须 kebab-case)。
|
|
127
|
+
- **唯一性**:同一个 `serverName` 在**全局与所有项目之间**全局唯一。重复时保存直接失败:
|
|
128
|
+
`serverName "xxx" 已被另一个 MCP 使用(全局或其他项目),请换一个名称`。
|
|
129
|
+
- **模型侧工具名**:`mcp__<serverName>__<原始工具名>`。这段前缀是隔离与可见性机制的判定依据,不要手工改 `serverName` 来"重命名工具"——那会生成一整套新工具名。
|
|
130
|
+
- **项目条目 id**:`vsm-mcp.<workspaceHash>.<serverName>`(旧版残留为 `vsm-mcp:<hash>:<name>`,插件识别但不显示为全局 MCP)。
|
|
131
|
+
- 项目 MCP 只接受**已注册为 DSH workspace** 的路径,否则报 `项目未注册为 DSH workspace,不能管理项目 MCP`。
|
|
132
|
+
|
|
133
|
+
## 6. 作用域隔离(项目 MCP 的边界)
|
|
134
|
+
|
|
135
|
+
项目 MCP 的连接由 Host 维护,但**每个 agent 只继承当前工作区的项目工具**:
|
|
136
|
+
|
|
137
|
+
- 机制:`tools.restrict({ deny })` 控制模型可见性 + `tools.guard()` 在执行层兜底拒绝。
|
|
138
|
+
- 当前对话工作区 ≠ 条目所属工作区时,调用被拒,返回:
|
|
139
|
+
`已拒绝:当前对话工作区不能使用其他项目的 MCP`
|
|
140
|
+
- 会话没有落在任何已注册工作区时:
|
|
141
|
+
`项目 MCP 需要在已注册工作区的对话中使用`
|
|
142
|
+
- **全局 MCP 不受此限制**,在任何会话都可用。
|
|
143
|
+
- 工作区匹配按**最长父路径**:在子目录会话里也能用到父级工作区的项目 MCP。
|
|
144
|
+
|
|
145
|
+
排查「工具有但没有出现在模型面前」时,先看当前会话 cwd 是否落在目标 workspace 内。
|
|
146
|
+
|
|
147
|
+
## 7. 故障排查
|
|
148
|
+
|
|
149
|
+
| 症状 | 原因与处理 |
|
|
150
|
+
|---|---|
|
|
151
|
+
| 卡片状态 `错误`,提示 `MCP 插件未正常运行` | loader 条目 fiber 未进入 ACTIVE。看 Host 启动日志中该条目的报错;多半是 `command` 不存在或启动即退出 |
|
|
152
|
+
| 状态 `连接中` 一直不变 | 服务端未响应 MCP 握手。核对 stdio 的 `command`/`args`/`cwd`,或 HTTP 的 URL 可达性 |
|
|
153
|
+
| 已连接但 `toolCount` 为 0 | 服务端 `tools/list` 返回空,或工具同步失败。先用该 MCP 的 CLI 自行验证能列出工具 |
|
|
154
|
+
| `stdio MCP 必须填写 command` | 表单「命令」为空 |
|
|
155
|
+
| `HTTP MCP 必须填写 http(s) URL` | 「URL」为空或不是 `http(s)://` 开头 |
|
|
156
|
+
| `serverName 只能包含字母、数字、下划线和连字符(最多 32 位)` | 名称含空格/点/中文或超长 |
|
|
157
|
+
| `serverName "x" 已被另一个 MCP 使用…` | 见 §5 唯一性 |
|
|
158
|
+
| 设置页里 `env` / `headers` 的值显示成 `••••••` | **这是脱敏掩码**(`publicConfig`)。读回的是掩码而非真实值,**不要把它原样回写保存**,否则会用掩码覆盖真实凭据 |
|
|
159
|
+
| `.mcp.json 解析失败:…` | 文件不是合法 JSON;插件只报错、**不覆盖**该文件,修好 JSON 后刷新 |
|
|
160
|
+
| `.mcp.json 的 mcpServers 必须是对象` | `mcpServers` 写成了数组/标量 |
|
|
161
|
+
| 项目显示「目录缺失」 | workspace 路径已不存在(项目被移动/删除) |
|
|
162
|
+
|
|
163
|
+
## 8. 边界与注意事项
|
|
164
|
+
|
|
165
|
+
- **删除/启停走设置页或 RPC**,不要手删 profile 树里的条目——插件按条目 id(`vsm-mcp.*`)识别项目 MCP,手工改名会让它被当成全局条目。
|
|
166
|
+
- **项目 MCP 的持久真相是 `.mcp.json`**:重启后插件按文件内容 reconcile 该项目的激活状态(文件里有的激活、文件里没有的停用)。
|
|
167
|
+
- 项目 MCP 的写操作固定写入 `<workspacePath>/.mcp.json`,不会写到别处。
|
|
168
|
+
- 全局 MCP 由 loader 落盘到 profile 树,重启自动恢复。
|
|
169
|
+
- MCP 条目本身是 loader 条目,**HMR/热更新会重建连接**;调试连接问题时可先 `mcp.refresh`(等价于用当前 config 重新 `loader.update`)。
|
|
170
|
+
- 本技能只覆盖 dsh-vscode-mode 的 MCP 管理面。若问题出在 MCP 服务端自身(协议实现、鉴权、工具 schema),那不是本插件的问题,应转向该服务端的调试手段。
|
|
171
|
+
|
|
172
|
+
## 9. 关键源码位置
|
|
173
|
+
|
|
174
|
+
需要在插件仓库里核对行为时(按需读取,不要一次全开):
|
|
175
|
+
|
|
176
|
+
- `src/mcp.ts` — 全局 MCP 的 list/save/remove/toggle/refresh,`validateConfig`,`publicConfig` 脱敏,`toolsOf` 工具前缀。
|
|
177
|
+
- `src/mcpProject.ts` — 项目 `.mcp.json` 读写、`configFromDef`、reconcile、条目 id 生成。
|
|
178
|
+
- `src/mcpIsolation.ts` — 工作区隔离(restrict + guard)与两条拒绝文案。
|
|
179
|
+
- `src/shared/mcp.ts` — 共享契约(`MpcConfig` / `MpcServer` / `MpcProject`)。
|
|
180
|
+
- `src/shared/rpc.ts` — `mcp.*` 方法的入参与返回形状。
|
|
181
|
+
- `src/client/ui/McpSettings.ts` — 设置页 UI(三个子页签与表单字段映射)。
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
[data-edrv-view] .edrv-search-pop { position: absolute; top: calc(100% + 4px); right: 8px; width: min(460px, calc(100vw - 24px)); max-height: 300px; overflow: auto; background: var(--dsw-alias-bg-overlay, #1c1c1c); border: 1px solid var(--dsw-alias-border-l2, #555); border-radius: 10px; box-shadow: 0 6px 20px rgba(0,0,0,.4); z-index: 60; padding: 4px; }
|
|
30
30
|
[data-edrv-view] .edrv-search-item { display: flex; flex-direction: column; gap: 1px; padding: 5px 8px; border-radius: 6px; cursor: pointer; }
|
|
31
31
|
[data-edrv-view] .edrv-search-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.07)); }
|
|
32
|
+
/* 键盘高亮项(↑↓ 选择):与 hover 同特异性但声明在后,故选中态稳定可辨 @author ddj 2026年09月11号 */
|
|
33
|
+
[data-edrv-view] .edrv-search-item.edrv-search-sel { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.14)); }
|
|
32
34
|
[data-edrv-view] .edrv-search-item .n { color: var(--dsw-alias-label-primary, #eee); font-size: 12px; word-break: break-all; }
|
|
33
35
|
[data-edrv-view] .edrv-search-item .d { color: var(--dsw-alias-label-tertiary, #888); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
34
36
|
[data-edrv-view] .edrv-search-empty { padding: 8px; font-size: 12px; color: var(--dsw-alias-label-tertiary, #888); }
|
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* dsh-vscode-mode client — QuickOpen:顶部搜索框(Ctrl+P 打开文件)。
|
|
4
4
|
* 快捷键随配置(edrv.quickOpen),占位文案同步当前键位。
|
|
5
|
+
* 候选浮窗支持鼠标点击与键盘导航(↑↓ 切换高亮、Enter 打开高亮项、Esc 关闭);
|
|
6
|
+
* 输入法组词中不消费按键(中文候选词确认优先)。
|
|
5
7
|
* 迁移自原 src/client/index.ts 的 QuickOpen,语义不改。
|
|
6
|
-
* 作者 ddj 2026-08-20 / 2026-08-26
|
|
8
|
+
* 作者 ddj 2026-08-20 / 2026-08-26 / 2026年09月11号
|
|
7
9
|
*/
|
|
8
10
|
import React from 'react'
|
|
9
11
|
import { rpc } from '../rpc.js'
|
|
10
12
|
import { bindingsOf, chordOf, matchEvent, useKeybindingsVersion } from '../keybindings.js'
|
|
13
|
+
import { stepIndex } from './searchNav.js'
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* 快速打开搜索框:Ctrl+P 聚焦,输入 ≥2 字符经 edrv.searchFiles 搜索,
|
|
14
|
-
*
|
|
17
|
+
* 结果列表点击 / ↑↓ 选择 + 回车打开高亮项。
|
|
15
18
|
* @param props.sessionId 会话 id
|
|
16
19
|
* @param props.onOpen 打开回调(path)
|
|
17
20
|
*/
|
|
@@ -21,11 +24,19 @@ export function QuickOpen(props) {
|
|
|
21
24
|
const [q, setQ] = React.useState('')
|
|
22
25
|
const [results, setResults] = React.useState(null)
|
|
23
26
|
const [open, setOpen] = React.useState(false)
|
|
27
|
+
const [active, setActive] = React.useState(0)
|
|
24
28
|
const inputRef = React.useRef(null)
|
|
29
|
+
const popRef = React.useRef(null)
|
|
30
|
+
const listId = React.useId()
|
|
25
31
|
const seq = React.useRef(0)
|
|
26
32
|
const timer = React.useRef(null)
|
|
27
33
|
useKeybindingsVersion() // 键位变化时刷新占位文案
|
|
28
34
|
|
|
35
|
+
const list = Array.isArray(results) ? results : []
|
|
36
|
+
const showPop = open && Array.isArray(results) && q.trim().length >= 2
|
|
37
|
+
// 高亮下标:候选集变化(含异步结果变短)时按当前长度收敛,不再单独改状态
|
|
38
|
+
const activeIdx = list.length ? Math.min(active, list.length - 1) : 0
|
|
39
|
+
|
|
29
40
|
const search = (query) => {
|
|
30
41
|
const s = ++seq.current
|
|
31
42
|
const clean = String(query || '').trim()
|
|
@@ -37,14 +48,14 @@ export function QuickOpen(props) {
|
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
const onChange = (v) => {
|
|
40
|
-
setQ(v); setOpen(true)
|
|
51
|
+
setQ(v); setActive(0); setOpen(true)
|
|
41
52
|
if (timer.current) clearTimeout(timer.current)
|
|
42
53
|
timer.current = setTimeout(() => search(v), 180)
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
const pick = (p) => {
|
|
46
57
|
onOpen(p)
|
|
47
|
-
setQ(''); setResults(null); setOpen(false)
|
|
58
|
+
setQ(''); setResults(null); setActive(0); setOpen(false)
|
|
48
59
|
inputRef.current?.blur?.()
|
|
49
60
|
}
|
|
50
61
|
|
|
@@ -70,20 +81,56 @@ export function QuickOpen(props) {
|
|
|
70
81
|
}
|
|
71
82
|
}, [])
|
|
72
83
|
|
|
84
|
+
// 高亮项滚入浮窗可视区(候选集或高亮变化时;nearest 避免整页跳动)
|
|
85
|
+
React.useEffect(() => {
|
|
86
|
+
if (!showPop) return
|
|
87
|
+
const node = popRef.current?.querySelector?.('.edrv-search-item.edrv-search-sel')
|
|
88
|
+
if (typeof node?.scrollIntoView === 'function') node.scrollIntoView({ block: 'nearest' })
|
|
89
|
+
}, [showPop, activeIdx, list.length])
|
|
90
|
+
|
|
73
91
|
const dirText = (p) => {
|
|
74
92
|
const base = String(p).split(/[\\/]/).pop() || String(p)
|
|
75
93
|
return String(p).slice(0, Math.max(0, String(p).length - base.length)).replace(/[\\/]+$/, '') || String(p)
|
|
76
94
|
}
|
|
77
95
|
|
|
96
|
+
/**
|
|
97
|
+
* 输入框按键:浮窗有候选时消费 ↑↓/Enter,其余情况放行(保留原生光标行为)。
|
|
98
|
+
* 输入法组词中一律放行——中文候选词的 Enter 确认不得被当成「打开文件」。
|
|
99
|
+
* @author ddj 2026年09月11号
|
|
100
|
+
* @param e 输入框键盘事件
|
|
101
|
+
*/
|
|
102
|
+
const onInputKey = (e) => {
|
|
103
|
+
if (e.nativeEvent?.isComposing) return
|
|
104
|
+
if (e.key === 'Escape') { setOpen(false); return }
|
|
105
|
+
if (!showPop || !list.length) return
|
|
106
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
107
|
+
e.preventDefault()
|
|
108
|
+
setActive((v) => stepIndex(v, e.key === 'ArrowDown' ? 1 : -1, list.length))
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
if (e.key === 'Enter') {
|
|
112
|
+
e.preventDefault()
|
|
113
|
+
pick(list[activeIdx])
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
78
117
|
let pop = null
|
|
79
|
-
if (
|
|
80
|
-
if (
|
|
81
|
-
const items =
|
|
118
|
+
if (showPop) {
|
|
119
|
+
if (list.length) {
|
|
120
|
+
const items = list.map((p, i) => React.createElement('div', {
|
|
121
|
+
key: p,
|
|
122
|
+
id: listId + '-' + i,
|
|
123
|
+
role: 'option',
|
|
124
|
+
'aria-selected': i === activeIdx,
|
|
125
|
+
className: 'edrv-search-item' + (i === activeIdx ? ' edrv-search-sel' : ''),
|
|
126
|
+
onMouseEnter: () => setActive(i),
|
|
127
|
+
onClick: () => pick(p),
|
|
128
|
+
},
|
|
82
129
|
React.createElement('span', { className: 'n' }, String(p).split(/[\\/]/).pop() || p),
|
|
83
130
|
React.createElement('span', { className: 'd' }, dirText(p))))
|
|
84
|
-
pop = React.createElement('div', { className: 'edrv-search-pop' }, ...items)
|
|
131
|
+
pop = React.createElement('div', { ref: popRef, id: listId + '-list', className: 'edrv-search-pop', role: 'listbox' }, ...items)
|
|
85
132
|
} else {
|
|
86
|
-
pop = React.createElement('div', { className: 'edrv-search-pop' }, React.createElement('div', { className: 'edrv-search-empty' }, '无匹配文件'))
|
|
133
|
+
pop = React.createElement('div', { ref: popRef, className: 'edrv-search-pop' }, React.createElement('div', { className: 'edrv-search-empty' }, '无匹配文件'))
|
|
87
134
|
}
|
|
88
135
|
}
|
|
89
136
|
|
|
@@ -91,12 +138,13 @@ export function QuickOpen(props) {
|
|
|
91
138
|
React.createElement('input', {
|
|
92
139
|
ref: inputRef, className: 'edrv-search', placeholder: '搜索文件 (' + (chordOf('edrv.quickOpen') ?? 'Ctrl+P') + ')',
|
|
93
140
|
value: q,
|
|
141
|
+
role: 'combobox',
|
|
142
|
+
'aria-expanded': showPop && list.length > 0,
|
|
143
|
+
'aria-controls': showPop && list.length ? listId + '-list' : undefined,
|
|
144
|
+
'aria-activedescendant': showPop && list.length ? listId + '-' + activeIdx : undefined,
|
|
94
145
|
onChange: (e) => onChange(e.target.value),
|
|
95
146
|
onFocus: () => { if (q.trim().length >= 2 && !results) setOpen(true) },
|
|
96
|
-
onKeyDown:
|
|
97
|
-
if (e.key === 'Enter' && Array.isArray(results) && results.length) pick(results[0])
|
|
98
|
-
if (e.key === 'Escape') setOpen(false)
|
|
99
|
-
},
|
|
147
|
+
onKeyDown: onInputKey,
|
|
100
148
|
}),
|
|
101
149
|
pop)
|
|
102
150
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 候选列表键盘导航纯函数。
|
|
3
|
+
* 从搜索浮窗(QuickOpen)的按键处理中拆出,便于单测;不依赖 React 与 DOM。
|
|
4
|
+
* 导航语义:步进越界**循环**(末位 ↓ 回首位、首位 ↑ 到末位),与命令栏
|
|
5
|
+
* (CommandPalette 的 (min+step) % len)保持一致。
|
|
6
|
+
* 作者 ddj 2026年09月11号
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 计算候选列表的下一选中下标(循环;任何非法输入都收敛到合法下标)。
|
|
11
|
+
* 入参与返回值恒在 `[0, length - 1]`(`length <= 0` 时返回 0)。
|
|
12
|
+
* 作者 ddj 2026年09月11号
|
|
13
|
+
* @param current 当前选中下标(越界/非有限值按首位处理)
|
|
14
|
+
* @param delta 步进(+1 下一项 / -1 上一项)
|
|
15
|
+
* @param length 候选总数
|
|
16
|
+
* @returns 步进后的选中下标;无候选时返回 0
|
|
17
|
+
*/
|
|
18
|
+
export function stepIndex(current: number, delta: number, length: number): number {
|
|
19
|
+
if (!Number.isFinite(length) || length <= 0) return 0
|
|
20
|
+
const size = Math.floor(length)
|
|
21
|
+
const from = Math.min(Math.max(Number.isFinite(current) ? Math.floor(current) : 0, 0), size - 1)
|
|
22
|
+
const move = Number.isFinite(delta) ? Math.trunc(delta) : 0
|
|
23
|
+
return ((from + move) % size + size) % size
|
|
24
|
+
}
|
package/src/compat.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { entriesOf } from './mcp.js'
|
|
|
13
13
|
import { loadSettingsDeps, settingsInstallNote, settingsInstallStrategy } from './fileOpenSettings.js'
|
|
14
14
|
import { readDevForm } from './devForm.js'
|
|
15
15
|
import { compareDshVersions, detectDshVersion, familyLabel, parseDshVersion } from './dshVersion.js'
|
|
16
|
+
import { SKILL_PREFIXES, skillGroupState } from './skills.js'
|
|
16
17
|
import type { CompatAdapter, CompatReport } from './shared/compat.js'
|
|
17
18
|
import type { Ctx } from './store.js'
|
|
18
19
|
import { ROUTE_PREFIX } from './paths.js'
|
|
@@ -55,11 +56,14 @@ export function detectExternal(ctx: Ctx, depsAvailable: boolean): CompatAdapter[
|
|
|
55
56
|
const settings = ctx.get('settings') as { describe?: unknown; update?: unknown } | undefined
|
|
56
57
|
const hasSettings = Boolean(settings?.describe || settings?.update)
|
|
57
58
|
const sub = ctx.get('subprocess') as { spawn?: unknown } | undefined
|
|
59
|
+
const skills = skillGroupState()
|
|
58
60
|
return [
|
|
59
61
|
{ name: MCP_PACKAGE, active: mcpCount > 0, note: mcpCount > 0 ? mcpCount + ' 个 MCP 服务条目' : '未检测到 MCP 条目(MCP 管理页显示为空)' },
|
|
60
62
|
{ name: '设置持久化(@deepseek-ai/dsh-settings)', active: depsAvailable, note: depsAvailable ? '设置 section 已安装' : '未安装:fileOpenTool 持久化降级为配置值' },
|
|
61
63
|
{ name: 'settings 服务', active: hasSettings, note: hasSettings ? '可读写设置' : '不可用(设置读写走配置回退)' },
|
|
62
64
|
{ name: '文件浏览器打开(subprocess 服务)', active: typeof sub?.spawn === 'function', note: typeof sub?.spawn === 'function' ? '可定位/打开 OS 文件浏览器' : '不可用(右键「在文件浏览器中打开」将提示失败)' },
|
|
65
|
+
// 新增项一律追加在末尾:既有下标被 tests/compat.test.ts 断言,不得前插。
|
|
66
|
+
{ name: '插件技能组(' + SKILL_PREFIXES[0] + '*)', active: skills.mounted, note: skills.note + (skills.mounted && skills.dir ? '(' + skills.dir + ')' : '') },
|
|
63
67
|
]
|
|
64
68
|
}
|
|
65
69
|
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { createLspRpc } from './lsp/rpc.js'
|
|
|
26
26
|
import { disposeAllServers } from './lsp/transport.js'
|
|
27
27
|
import { createAiRpc } from './ai/rpc.js'
|
|
28
28
|
import { installRulesSection } from './rules.js'
|
|
29
|
+
import { installSkillGroup } from './skills.js'
|
|
29
30
|
import type { RpcHandlerMap } from './shared/rpc.js'
|
|
30
31
|
import type { Registry } from './registry.js'
|
|
31
32
|
import type { Ctx } from './store.js'
|
|
@@ -55,6 +56,9 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
55
56
|
/** 规则注入 section(~/.dsh/rules 与 <工作区>/.dsh/rules;旧版 DSH 无 systemPrompt 时静默降级)。 */
|
|
56
57
|
const rulesInstalled = installRulesSection(ctx)
|
|
57
58
|
if (!rulesInstalled) log.warn('未检测到 systemPrompt 服务,规则仅可管理不注入')
|
|
59
|
+
/** 插件自带技能组(<包根>/skills,惰性获取 skills 服务;返回值仅表示"已调度",实际结果见 skillGroupState)。 */
|
|
60
|
+
const skillsDispatched = installSkillGroup(ctx)
|
|
61
|
+
if (!skillsDispatched) log.warn('技能组未调度,插件技能组不可用')
|
|
58
62
|
/** LSP RPC 与会话清理(一次性创建,tracker 状态跨请求保留)。 */
|
|
59
63
|
const lspRpc = createLspRpc({ ctx, pluginConfig: config, manager: lspManager })
|
|
60
64
|
const lspHandlers: Partial<RpcHandlerMap> = lspRpc.handlers
|
|
@@ -92,7 +96,7 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
92
96
|
disposeAllServers()
|
|
93
97
|
})
|
|
94
98
|
|
|
95
|
-
log.info('编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP 已接入,规则注入' + (rulesInstalled ? '已接入' : '未接入') + ')')
|
|
99
|
+
log.info('编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP 已接入,规则注入' + (rulesInstalled ? '已接入' : '未接入') + ',技能组' + (skillsDispatched ? '装配中' : '未装配') + ')')
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
/**
|
package/src/paths.ts
CHANGED
|
@@ -173,6 +173,17 @@ export function vendorDirOf(moduleUrl: string): string {
|
|
|
173
173
|
return join(assetsDirOf(moduleUrl), 'vendor')
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* 插件自带技能组根目录(包根 skills/,与 assets/ 同级;随包发布)。
|
|
178
|
+
* 布局:<根>/<技能名>/SKILL.md(目录式)或 <根>/<技能名>.md(扁平式)。
|
|
179
|
+
* @author ddj 2026年09月11号
|
|
180
|
+
* @param moduleUrl 模块 URL(缺省 import.meta.url;测试注入)
|
|
181
|
+
* @returns 技能组根目录绝对路径
|
|
182
|
+
*/
|
|
183
|
+
export function skillsDirOf(moduleUrl: string): string {
|
|
184
|
+
return join(dirname(fileURLToPath(moduleUrl)), '..', 'skills')
|
|
185
|
+
}
|
|
186
|
+
|
|
176
187
|
/** 图标目录:config.imageDir 覆盖优先,否则插件包 assets/。 */
|
|
177
188
|
export function imageDirOf(config: unknown, moduleUrl: string): string {
|
|
178
189
|
const cfg = config as { imageDir?: unknown } | undefined
|