dsh-vscode-mode 0.16.0 → 0.18.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/README.md +15 -9
- package/lib/client.js +954 -133
- package/lib/client.js.map +1 -1
- package/lib/index.js +5026 -4387
- package/lib/index.js.map +1 -1
- package/locale/en.json +4 -2
- package/locale/zh.json +4 -2
- package/package.json +4 -2
- package/skills/dsh-vscodemode-mcp/SKILL.md +24 -7
- package/src/capture.ts +2 -2
- package/src/client/compat.ts +2 -1
- package/src/client/index.ts +37 -7
- package/src/client/localeClient.ts +97 -0
- package/src/client/mcpSessionState.ts +108 -0
- package/src/client/monaco/lsp/providers.ts +32 -3
- package/src/client/officialSidebar.ts +4 -1
- package/src/client/state/modelWarmer.ts +198 -0
- package/src/client/state/recordsFeed.ts +241 -0
- package/src/client/styles/editor.css +5 -1
- package/src/client/ui/ConversationDiffDock.ts +10 -19
- package/src/client/ui/DiffBadge.ts +12 -19
- package/src/client/ui/EditorPane.ts +16 -4
- package/src/client/ui/EditorView.ts +114 -51
- package/src/client/ui/McpSessionStatus.ts +75 -0
- package/src/client/ui/McpSettings.ts +47 -13
- package/src/client/ui/PerfSizeBadge.ts +7 -4
- package/src/client/ui/PluginDetailSection.ts +82 -0
- package/src/dshVersion.ts +4 -2
- package/src/fileVersions.ts +2 -2
- package/src/lsp/rpc.ts +2 -2
- package/src/mcp.ts +1 -0
- package/src/mcpBridge.ts +163 -0
- package/src/mcpDiscover.ts +38 -0
- package/src/mcpPool.ts +245 -0
- package/src/mcpProject.ts +15 -12
- package/src/mcpRuntime.ts +301 -255
- package/src/registry.ts +15 -2
- package/src/rpc.ts +15 -3
- package/src/rules.ts +9 -4
- package/src/search/fallback.ts +2 -2
- package/src/search/ripgrep.ts +2 -2
- package/src/shared/mcp.ts +47 -0
- package/src/shared/rpc.ts +5 -1
- package/src/store.ts +26 -1
- package/src/svn.ts +2 -2
package/locale/en.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "VSCodeMode",
|
|
4
|
+
"description": "VSCode-like coding on DSH: Monaco editor with tabs and QuickOpen, command palette, agent edit review, SVN integration, and LSP smartness."
|
|
5
|
+
}
|
|
4
6
|
}
|
package/locale/zh.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "VSCodeMode",
|
|
4
|
+
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(页签/QuickOpen/状态栏)、命令栏、Agent 编辑差异审查、SVN 集成与 LSP 智能。"
|
|
5
|
+
}
|
|
4
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车,界面状态(页签/光标/侧栏/编辑 Tab/导航历史)镜像到 DSH home 跨重启恢复",
|
|
5
5
|
"icon": "assets/icon.svg",
|
|
6
6
|
"keywords": [
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"type": "module",
|
|
31
31
|
"engines": {
|
|
32
|
-
"node": "^22.19.0 || >=24.0.0"
|
|
32
|
+
"node": "^22.19.0 || >=24.0.0",
|
|
33
|
+
"dsh": ">=0.1.0-rc.1 <0.3.0-0"
|
|
33
34
|
},
|
|
34
35
|
"packageManager": "pnpm@11.3.0",
|
|
35
36
|
"main": "./lib/index.js",
|
|
@@ -90,6 +91,7 @@
|
|
|
90
91
|
"exports": {
|
|
91
92
|
".": "./lib/index.js",
|
|
92
93
|
"./client": "./lib/client.js",
|
|
94
|
+
"./locale/*.json": "./locale/*.json",
|
|
93
95
|
"./package.json": "./package.json"
|
|
94
96
|
},
|
|
95
97
|
"dsh": {
|
|
@@ -14,8 +14,8 @@ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置
|
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| 持久配置 | profile loader 树 | 已注册 workspace 根目录的 `.mcp.json` |
|
|
16
16
|
| 生效范围 | profile 内所有工作区 | cwd 匹配该 workspace 的 agent |
|
|
17
|
-
| 运行方式 | 原有全局 loader entry |
|
|
18
|
-
| 连接实例 | 每个启用的全局 entry 一份 |
|
|
17
|
+
| 运行方式 | 原有全局 loader entry | 私有连接 scope + 会话授权代理,复用官方 MCP 插件 |
|
|
18
|
+
| 连接实例 | 每个启用的全局 entry 一份 | 默认逐 agent 隔离;显式选择 workspace 后工作区复用 |
|
|
19
19
|
| 没有活动 agent | 仍可运行 | 仅保存配置,`configured`、`instanceCount: 0` |
|
|
20
20
|
| 团队共享 | 否 | `.mcp.json` 可以随仓库共享 |
|
|
21
21
|
|
|
@@ -25,7 +25,9 @@ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置
|
|
|
25
25
|
|
|
26
26
|
设置 → VSCodeMode → MCP 管理,包含「我的 MCP」「项目 MCP」「MCP 市场」(占位)。项目服务在所选项目内管理。
|
|
27
27
|
|
|
28
|
-
保存项目服务会写入 `.mcp.json`
|
|
28
|
+
保存项目服务会写入 `.mcp.json` 并更新匹配 agent 的能力目录与惰性资源入口。新建会话只等待配置准备和轻量入口,不等待任何项目 MCP 握手;默认 `on-demand`,未使用不启动。显式 `background` 后台预热也不阻塞创建。同一 Host 最多两个冷启动;同代次初始化 single-flight。配置写操作仍串行保护,但连接准备不占有跨会话全局锁。
|
|
29
|
+
|
|
30
|
+
AI 先调用全局发现入口 `discover_project_mcp {action:"list"}`,仅返回执行 agent 工作区内的安全目录;需要时 `{action:"connect",serverName:"..."}` 连接并返回真实 schema。入口从工具执行上下文取 agent,不接受 sessionId、路径、命令、URL 或凭据。真实工具依然走官方审批、guard 和结果投影;Native 在下一次请求获得工具,PTC 必须结束当前程序后新建程序刷新 SDK,禁止 eval 或通用透传绕过权限。资源工具可通过所属工作区的惰性 provider 按需连接。
|
|
29
31
|
|
|
30
32
|
禁用、删除、配置变更、刷新重连及 agent/插件卸载都会释放相应 fiber 和连接。仅刷新未变化的项目列表不会重复连接。主机代码更新需要重启 DSH 才能加载新实现。
|
|
31
33
|
|
|
@@ -33,6 +35,14 @@ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置
|
|
|
33
35
|
|
|
34
36
|
表单 stdio 参数采用 JSON `string[]` 或每行一个完整参数,不按空格/引号做 shell 拆分;含空参数或内嵌换行使用 JSON。HTTP 请求头每行 `key=value`,保留值内的等号,拒绝非法/重复(忽略大小写)键。失败保存保持表单,忙时禁止重复动作。
|
|
35
37
|
|
|
38
|
+
### 会话列表状态
|
|
39
|
+
|
|
40
|
+
官方 `sidebar.session.row.leading` 插槽在会话标题前显示 16px 加载或异常图标;平稳完成不常驻绿色成功标。官方运行中、待审批、新消息状态优先覆盖 leading,归档行不展示 leading;`sidebar.session.row.hover` 悬浮详情仍可查看。
|
|
41
|
+
|
|
42
|
+
详情只描述 VSCodeMode 管理的**项目 MCP**:待使用(不转圈)、读取配置、等待启动名额、加载插件模块、等待服务插件启动、复用已有连接、释放旧实例、完成或异常,展示完成数、耗时与安全错误摘要。共享显示会话本次等待,不将连接年龄冒充本次等待。超过 10 秒仅提示耗时较长,不判定失败。完成文案为「初始化完成,连接健康未验证」。无活跃 agent 文案为「当前无活跃 MCP 实例」,不是成功;不包含全局或 preset MCP。
|
|
43
|
+
|
|
44
|
+
所有可见行共用一份 `mcp.sessionSnapshot` 订阅,在文档可见且有消费者时按 2.5 秒间隔查询;请求 10 秒截止、不重叠,隐藏/退订/卸载取消,过期响应丢弃。错误保留上次快照并标过期。查询只读内存,**不读配置、不 reconcile、不激活/保留历史会话、不建立连接**;旧 Host 或缺少插槽时安全降级。错误详情不返回 env、headers、完整配置或凭据 URL,仅返回安全类别提示,原始诊断看 Host 日志。
|
|
45
|
+
|
|
36
46
|
## 3. 项目配置
|
|
37
47
|
|
|
38
48
|
`.mcp.json` 示例:
|
|
@@ -58,6 +68,8 @@ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置
|
|
|
58
68
|
- stdio:`command` 必填;可填 `args`、`cwd`、`env`。未指定 `cwd` 时,以项目根作为进程工作目录。
|
|
59
69
|
- HTTP:`url` 必须以 `http://` 或 `https://` 开头,可填 `headers`。
|
|
60
70
|
- 高级参数:`toolCallTimeoutMs`、`failOnStartupError`、`maxInstructionBytes`、`reconnect`。
|
|
71
|
+
- 项目扩展 `vscodeMode: { startup: "on-demand" | "background", sharing: "agent" | "workspace", startupTimeoutMs: 60000 }` 与官方传输配置分离,不透传;默认 on-demand + agent,旧文件不批量改写。初始化预算接受 1000–300000 整数,不是业务调用超时。全局配置不接受项目扩展。
|
|
72
|
+
- workspace 共享必须显式确认服务适合共享内部状态;不会自动给 CodeGraph 开启。池键含工作区、有效配置和策略,凭据只做内存摘要身份。共享调用默认串行,最后租约释放保温 60 秒;agent 模式立即释放。禁用/删除/刷新/配置变化先失效与撤销再关闭,关闭未确认则阻止替代。超时显示释放阶段,失败冷却 5 秒后允许显式重试,不无限自动重试。
|
|
61
73
|
- `disabled: true` 保持配置但不创建连接;保存表单会恢复启用。重启和 reconcile 都尊重 `disabled`。
|
|
62
74
|
- 保存保留文档顶层、其他服务器及当前服务器的未知字段和未编辑高级参数;切换传输方式会移除旧传输专属字段。
|
|
63
75
|
- `env` / `headers` 在返回值中使用 `••••••` 脱敏。原键的未修改掩码保存时恢复真实值;新键只有掩码而没有原值时会拒绝保存,需填写凭据。
|
|
@@ -85,12 +97,14 @@ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置
|
|
|
85
97
|
|---|---|---|
|
|
86
98
|
| `mcp.list` | `{}` | `servers` |
|
|
87
99
|
| `mcp.snapshot` | `{}` | `servers`, `projects`(只读运行快照) |
|
|
100
|
+
| `mcp.sessionSnapshot` | `{}` | `sessions`, `sampledAt`(按 sessionId 区分的活跃初始化状态,只读内存) |
|
|
88
101
|
| `mcp.save` | `{ config }` | `server` |
|
|
89
102
|
| `mcp.remove` | `{ id }` | 无 |
|
|
90
103
|
| `mcp.toggle` | `{ id, enabled }` | `server` |
|
|
91
104
|
| `mcp.refresh` | `{ id }` | `server` |
|
|
92
105
|
| `mcp.projects` | `{}` | `projects` |
|
|
93
|
-
| `mcp.projectSave` | `{ workspacePath, serverName, config }` | `project` |
|
|
106
|
+
| `mcp.projectSave` | `{ workspacePath, serverName, config, policy? }` | `project` |
|
|
107
|
+
| `mcp.projectConnect` | `{ sessionId, workspacePath, serverName }` | `project`(仅已活跃匹配会话,不激活历史会话) |
|
|
94
108
|
| `mcp.projectRemove` | `{ workspacePath, serverName }` | `project` |
|
|
95
109
|
| `mcp.projectToggle` | `{ workspacePath, serverName, enabled }` | `project` |
|
|
96
110
|
| `mcp.projectRefresh` | `{ workspacePath, serverName }` | `project` |
|
|
@@ -109,7 +123,7 @@ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置
|
|
|
109
123
|
|
|
110
124
|
## 6. 工具、资源和指令隔离
|
|
111
125
|
|
|
112
|
-
官方 0.1.7-rc.2 的 MCP Config 没有关闭资源/指令的开关;官方注册随 `scopeOf(ctx)`
|
|
126
|
+
官方 0.1.7-rc.2 的 MCP Config 没有关闭资源/指令的开关;官方注册随 `scopeOf(ctx)` 归属。因此连接在独立 scope 中隔离 tools、mcpResources、systemPrompt,捕获注册后再给合法 agent 提供代理,绝不将原始项目注册暴露全局。创建私有 scope 后先等待 backing fiber 初始激活完成再 provide,防止依赖服务丢失;真实官方模块测试覆盖此边界。
|
|
113
127
|
|
|
114
128
|
子 agent 会继承父 scope;跨 cwd 子会话还有三层处理:
|
|
115
129
|
|
|
@@ -126,7 +140,7 @@ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置
|
|
|
126
140
|
|
|
127
141
|
| 状态/现象 | 含义与处理 |
|
|
128
142
|
|---|---|
|
|
129
|
-
| `configured` |
|
|
143
|
+
| `configured` | 中性色;配置已保存,尚无连接实例,首次发现/资源访问或显式手动连接才启动(background 策略除外) |
|
|
130
144
|
| `disabled` | 配置已停用,不建立项目连接 |
|
|
131
145
|
| `connecting` | 官方插件还在等待启动完成;检查命令、cwd、URL 和握手 |
|
|
132
146
|
| `unverified` | 中性色;fiber 已激活,但官方没有公开连接状态证据,不能宣称已连接;右侧绿色开关只表示启用 |
|
|
@@ -140,7 +154,10 @@ whenToUse: 用户要求新增、修改、启停或排查 VSCodeMode MCP 配置
|
|
|
140
154
|
|
|
141
155
|
## 8. 维护位置与验证
|
|
142
156
|
|
|
143
|
-
- `src/mcpRuntime.ts
|
|
157
|
+
- `src/mcpRuntime.ts`:会话租约/能力台账、官方模块加载、非阻塞接入、迁移与卸载。
|
|
158
|
+
- `src/mcpPool.ts`:独立连接、single-flight、并发名额、取消/超时/保温和代次围栏。
|
|
159
|
+
- `src/mcpBridge.ts`:私有服务隔离、真实工具代理、动态列表和撤销。
|
|
160
|
+
- `src/mcpDiscover.ts`:统一发现入口与按 scope 输出的能力目录。
|
|
144
161
|
- `src/mcpProject.ts`:源配置读取、保留字段、命名检查及 reconcile。
|
|
145
162
|
- `src/mcpIsolation.ts`:继承工具限制、资源执行 guard、指令和资源列表覆盖。
|
|
146
163
|
- `src/mcp.ts`:全局 loader CRUD、脱敏与诚实状态。
|
package/src/capture.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { Ctx, Session } from './store.js'
|
|
|
16
16
|
import { READ_CAP, archiveCanceled, loadBucket, saveBucket } from './store.js'
|
|
17
17
|
import { fileMaxBatch, prune } from './model.js'
|
|
18
18
|
import type { Registry } from './registry.js'
|
|
19
|
-
import {
|
|
19
|
+
import { cwdOfSession } from './registry.js'
|
|
20
20
|
import { findRemoteWs, mirrorTargetOf } from './remoteWorkspace.js'
|
|
21
21
|
import { invalidateIndex } from './treeIndex.js'
|
|
22
22
|
import { log } from './log.js'
|
|
@@ -275,7 +275,7 @@ export async function captureToolResult(ctx: Ctx, registry: Registry, exec: any,
|
|
|
275
275
|
// remote_ssh_write 只依赖参数与 isError(其 result.value 形状不受本插件约束)
|
|
276
276
|
if (exec.name !== 'remote_ssh_write' && !result?.value) return
|
|
277
277
|
try {
|
|
278
|
-
const cwd =
|
|
278
|
+
const cwd = cwdOfSession(ctx, session)
|
|
279
279
|
if (!cwd) return
|
|
280
280
|
const args = ((exec.arguments || {}) as Record<string, unknown>)
|
|
281
281
|
const record = exec.name === 'remote_ssh_write'
|
package/src/client/compat.ts
CHANGED
package/src/client/index.ts
CHANGED
|
@@ -20,6 +20,9 @@ import { DiffBadge } from './ui/DiffBadge.js'
|
|
|
20
20
|
import { PerfSizeBadge } from './ui/PerfSizeBadge.js'
|
|
21
21
|
import { ConversationDiffDock } from './ui/ConversationDiffDock.js'
|
|
22
22
|
import { McpSettings } from './ui/McpSettings.js'
|
|
23
|
+
import { mcpSessionState } from './mcpSessionState.js'
|
|
24
|
+
import { McpSessionIcon, McpSessionDetail, type McpRowProps } from './ui/McpSessionStatus.js'
|
|
25
|
+
import { PluginDetailSection } from './ui/PluginDetailSection.js'
|
|
23
26
|
import { rpc } from './rpc.js'
|
|
24
27
|
import { loadMonaco } from './monaco/loader.js'
|
|
25
28
|
import { observeScheme, schemeOfSnapshot } from './monaco/theme.js'
|
|
@@ -60,6 +63,7 @@ import { SHORTCUT_PROFILES, defaultKeybindings, normalizeKeybindings } from '../
|
|
|
60
63
|
import { sidebarMinApply } from './sidebarMin.js'
|
|
61
64
|
import { editorLimitApply } from './editorLimit.js'
|
|
62
65
|
import { log } from './log.js'
|
|
66
|
+
import { installLocale } from './localeClient.js'
|
|
63
67
|
import { setupLsp, setSession, disposeLsp } from './monaco/lsp/index.js'
|
|
64
68
|
import { disposeSnippets } from './snippets/provider.js'
|
|
65
69
|
import { disposeLaunchJson } from './dap/launchSnippetProvider.js'
|
|
@@ -123,6 +127,10 @@ function setupCommands(ctx: any): CommandBridge {
|
|
|
123
127
|
export async function apply(ctx: any): Promise<void> {
|
|
124
128
|
const schedule = (fn: () => void, ms: number) => ctx.timeout(fn, ms)
|
|
125
129
|
|
|
130
|
+
// UI 壳层文案:注册本插件词典到官方 locale 服务(旧版无该服务时直读中文词典)。
|
|
131
|
+
// slot label / 编辑 Tab 标题一律写成 thunk,让语言切换后无需重注册即可刷新。
|
|
132
|
+
const t = installLocale(ctx)
|
|
133
|
+
|
|
126
134
|
/**
|
|
127
135
|
* 无门控上报界面状态镜像事件(绕过客户端 edrv.debug 开关,直接落 host 诊断日志)。
|
|
128
136
|
* 开关关闭时 dbg 直接 return,「重启后没恢复」这类问题会在客户端侧彻底无痕,故另开一条通路。
|
|
@@ -506,7 +514,7 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
506
514
|
name: 'conversation.view',
|
|
507
515
|
id: 'edrv-editor',
|
|
508
516
|
order: 5,
|
|
509
|
-
label: '
|
|
517
|
+
label: () => t('slot.fileEditor'),
|
|
510
518
|
inject: (sessionId: string) => ({ sessionId }),
|
|
511
519
|
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, fileOpeners: registry, sessions })))
|
|
512
520
|
}
|
|
@@ -530,9 +538,10 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
530
538
|
service: official.service,
|
|
531
539
|
slots: ctx.slots,
|
|
532
540
|
renderTab: officialRenderTab,
|
|
541
|
+
title: () => t('slot.fileEditor'),
|
|
533
542
|
guide: {
|
|
534
|
-
title: () =>
|
|
535
|
-
description: () => '
|
|
543
|
+
title: () => t('guide.title'),
|
|
544
|
+
description: () => t('guide.description'),
|
|
536
545
|
},
|
|
537
546
|
})
|
|
538
547
|
if (outcome === null) {
|
|
@@ -627,21 +636,33 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
627
636
|
}
|
|
628
637
|
retrySideService()
|
|
629
638
|
|
|
639
|
+
// Root row slots receive only sessionId: no sessions.scope()/activation for historical rows.
|
|
640
|
+
ctx.effect(() => {
|
|
641
|
+
const store = mcpSessionState()
|
|
642
|
+
/** @author ddj 2026年10月20号 @param props Official root row identity. @returns Leading marker with shared polling. */
|
|
643
|
+
const renderIcon = (props: unknown) => React.createElement(McpSessionIcon, { ...(props as McpRowProps), store })
|
|
644
|
+
/** @author ddj 2026年10月20号 @param props Official hover identity. @returns Project-only initialization details. */
|
|
645
|
+
const renderDetail = (props: unknown) => React.createElement(McpSessionDetail, { ...(props as McpRowProps), store })
|
|
646
|
+
const leading = registerSlotSafely(ctx, { name: 'sidebar.session.row.leading', id: 'edrv-mcp-status', order: 20 }, renderIcon)
|
|
647
|
+
const hover = registerSlotSafely(ctx, { name: 'sidebar.session.row.hover', id: 'edrv-mcp-detail', order: 20 }, renderDetail)
|
|
648
|
+
return () => { leading?.(); hover?.(); store.dispose() }
|
|
649
|
+
}, 'vscode-mode: session MCP status')
|
|
650
|
+
|
|
630
651
|
// 对话输入框上方差异 dock:普通对话显示单文案按钮,文件编辑页由 EditorView 隐藏
|
|
631
652
|
registerSlotSafely(ctx, {
|
|
632
653
|
name: 'conversation.input.dock',
|
|
633
654
|
id: 'edrv-diff-dock',
|
|
634
655
|
order: 30,
|
|
635
|
-
label: '
|
|
656
|
+
label: () => t('slot.diff'),
|
|
636
657
|
inject: (sessionId: string) => ({ sessionId }),
|
|
637
658
|
}, (props: unknown) => React.createElement(ConversationDiffDock, Object.assign({}, props)))
|
|
638
659
|
|
|
639
660
|
// header 差异角标:仅当前工作区存在差异时渲染
|
|
640
|
-
registerSlotSafely(ctx, { name: 'conversation.session.header.utilities', id: 'edrv-diff-badge', order: 90, label: '
|
|
661
|
+
registerSlotSafely(ctx, { name: 'conversation.session.header.utilities', id: 'edrv-diff-badge', order: 90, label: () => t('slot.diff') },
|
|
641
662
|
(props: unknown) => React.createElement(DiffBadge, Object.assign({}, props)))
|
|
642
663
|
|
|
643
664
|
// header 会话体积指示器:当前会话持久化体积,超阈值提示新开会话(性能优化)
|
|
644
|
-
registerSlotSafely(ctx, { name: 'conversation.session.header.utilities', id: 'edrv-perf-size', order: 89, label: '
|
|
665
|
+
registerSlotSafely(ctx, { name: 'conversation.session.header.utilities', id: 'edrv-perf-size', order: 89, label: () => t('slot.sessionSize') },
|
|
645
666
|
(props: unknown) => React.createElement(PerfSizeBadge, Object.assign({}, props, { sessions })))
|
|
646
667
|
|
|
647
668
|
// 设置页中的 VSCodeMode 专属页签,MCP 管理作为该页签的内部子 Tab。
|
|
@@ -649,15 +670,24 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
649
670
|
name: 'settings.section',
|
|
650
671
|
id: 'vscode-mode',
|
|
651
672
|
order: 30,
|
|
652
|
-
label: '
|
|
673
|
+
label: () => t('slot.settings'),
|
|
653
674
|
}, () => React.createElement(SettingsContext.Provider, { value: bridge.scope() }, React.createElement(McpSettings, {
|
|
654
675
|
openerRegistry: registry,
|
|
676
|
+
getMcpSession: () => readSessionScope(ctx).sessionId,
|
|
655
677
|
compatSummary,
|
|
656
678
|
// 快捷键统一走 DSH 官方配置:通用页只提供「打开官方快捷键配置」入口。
|
|
657
679
|
// 服务运行时探测(自愈):旧版 DSH 无该服务时按钮给出手动打开方式。
|
|
658
680
|
getShortcuts: () => detectShortcuts(ctx),
|
|
659
681
|
})))
|
|
660
682
|
|
|
683
|
+
// 官方插件管理器详情页的只读状态段(DSH 0.2.1 新增扩展位):非本插件页返回 null。
|
|
684
|
+
// 该 slot 在旧版 DSH 不存在,slots.inject 会一直等待(不报错),故无需版本判断。
|
|
685
|
+
registerSlotSafely(ctx, {
|
|
686
|
+
name: 'plugins.detail.section',
|
|
687
|
+
id: 'edrv-plugin-status',
|
|
688
|
+
order: 40,
|
|
689
|
+
}, (props: unknown) => React.createElement(PluginDetailSection, Object.assign({}, props)))
|
|
690
|
+
|
|
661
691
|
// 卸载收尾(G4,DSH 0.1.6-alpha.2 起支持运行时卸载/重载):
|
|
662
692
|
// 注销挂在存活的 window.monaco 上的全部 Monaco provider 并复位模块状态。
|
|
663
693
|
// 不做会导致重载后重复注册(补全/跳转/hover 各翻倍)与陈旧开关残留。
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — UI 壳层文案的 locale 接入。
|
|
3
|
+
* 此前 `locale` 写在 inject 里却零调用,界面文案全为硬编码中文;
|
|
4
|
+
* 这里只覆盖壳层(slot label、编辑 Tab 标题、侧栏引导),编辑器内部大段文案不动。
|
|
5
|
+
* 所有对外函数都对 locale 服务做可选探测:旧版 DSH 无该服务时回落中文词典。
|
|
6
|
+
* 作者 ddj 2026年10月19号
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** locale 命名空间(与包名一致,避免与其他插件串词条)。 */
|
|
10
|
+
export const LOCALE_NS = 'dsh-vscode-mode'
|
|
11
|
+
|
|
12
|
+
/** 支持的语言;zh 为缺省回退语言。 */
|
|
13
|
+
export const LOCALE_FALLBACK = 'zh'
|
|
14
|
+
|
|
15
|
+
/** 壳层文案词典(键名即 slot label / Tab 标题的稳定标识)。 */
|
|
16
|
+
export const LOCALE_DICTS: Record<string, Record<string, string>> = {
|
|
17
|
+
zh: {
|
|
18
|
+
'slot.fileEditor': '文件编辑',
|
|
19
|
+
'slot.diff': '差异',
|
|
20
|
+
'slot.sessionSize': '会话体积',
|
|
21
|
+
'slot.settings': 'VSCodeMode',
|
|
22
|
+
'guide.title': '文件编辑',
|
|
23
|
+
'guide.description': 'Monaco 文件编辑器:对话与编辑同屏(差异审查/跳转/大纲)',
|
|
24
|
+
},
|
|
25
|
+
en: {
|
|
26
|
+
'slot.fileEditor': 'File editor',
|
|
27
|
+
'slot.diff': 'Diff',
|
|
28
|
+
'slot.sessionSize': 'Session size',
|
|
29
|
+
'slot.settings': 'VSCodeMode',
|
|
30
|
+
'guide.title': 'File editor',
|
|
31
|
+
'guide.description': 'Monaco file editor: chat and editing side by side (diff review, jump, outline)',
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** locale 服务的最小结构面(结构性探测,不 import 官方类型)。 */
|
|
36
|
+
export interface LocaleLike {
|
|
37
|
+
register?: (ns: string, dicts: Record<string, Record<string, string>>) => unknown
|
|
38
|
+
bind?: (ns: string) => (key: string) => string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 翻译函数:键 → 当前语言文案(找不到时回落中文,再回落键本身)。 */
|
|
42
|
+
export type Translate = (key: string) => string
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 取词条:先当前语言、再中文、最后键本身。
|
|
46
|
+
* @author ddj 2026年10月19号
|
|
47
|
+
* @param lang 语言 id
|
|
48
|
+
* @param key 词条键
|
|
49
|
+
* @returns 文案
|
|
50
|
+
*/
|
|
51
|
+
function lookup(lang: string, key: string): string {
|
|
52
|
+
return LOCALE_DICTS[lang]?.[key] ?? LOCALE_DICTS[LOCALE_FALLBACK]?.[key] ?? key
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 生成不依赖 locale 服务的直读翻译函数(服务缺失/注册失败时使用)。
|
|
57
|
+
* @author ddj 2026年10月19号
|
|
58
|
+
* @param lang 语言 id
|
|
59
|
+
* @returns 翻译函数
|
|
60
|
+
*/
|
|
61
|
+
export function directTranslate(lang: string = LOCALE_FALLBACK): Translate {
|
|
62
|
+
return (key: string) => lookup(lang, key)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 注册本插件词典并返回翻译函数。
|
|
67
|
+
* 服务缺失、重复注册或 bind 抛错都不影响插件其余部分:一律回落直读词典。
|
|
68
|
+
* @author ddj 2026年10月19号
|
|
69
|
+
* @param ctx DSH 客户端上下文
|
|
70
|
+
* @returns 翻译函数(读取时才解析当前语言,可直接放进 slot label thunk)
|
|
71
|
+
*/
|
|
72
|
+
export function installLocale(ctx: { get: (name: string) => unknown }): Translate {
|
|
73
|
+
let service: LocaleLike | undefined
|
|
74
|
+
try {
|
|
75
|
+
service = ctx.get('locale') as LocaleLike | undefined
|
|
76
|
+
} catch {
|
|
77
|
+
// 服务缺失或探测抛错:后续一律按未命中处理。
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 注册失败(例如同名 ns 已被占)不应连带废掉 bind,故两段各自 try。
|
|
81
|
+
try {
|
|
82
|
+
if (service && typeof service.register === 'function') service.register(LOCALE_NS, LOCALE_DICTS)
|
|
83
|
+
} catch {
|
|
84
|
+
// 词典未注册成功:仍尝试 bind,最后还有直读词典兜底。
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
if (service && typeof service.bind === 'function') {
|
|
89
|
+
const bound = service.bind(LOCALE_NS)
|
|
90
|
+
if (typeof bound === 'function') return (key: string) => bound(key)
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// bind 异常按未命中处理。
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return directTranslate()
|
|
97
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** Shared, read-only sidebar MCP polling. @author ddj 2026年10月20号 */
|
|
2
|
+
import { rpc } from './rpc.js'
|
|
3
|
+
import type { McpSessionStatus } from '../shared/mcp.js'
|
|
4
|
+
|
|
5
|
+
export interface SessionMcpView { status?: McpSessionStatus; stale: boolean }
|
|
6
|
+
type Snapshot = { sessions: McpSessionStatus[]; sampledAt: number }
|
|
7
|
+
type FetchStatus = (signal: AbortSignal) => Promise<Snapshot>
|
|
8
|
+
const unavailable: SessionMcpView = { stale: true }
|
|
9
|
+
const inactive: SessionMcpView = { stale: false }
|
|
10
|
+
|
|
11
|
+
/** One poller for all mounted row/hover consumers; never retains or activates sessions. */
|
|
12
|
+
export class McpSessionState {
|
|
13
|
+
private views = new Map<string, SessionMcpView>()
|
|
14
|
+
private listeners = new Map<string, Set<() => void>>()
|
|
15
|
+
private timer?: ReturnType<typeof setTimeout>
|
|
16
|
+
private request?: AbortController
|
|
17
|
+
private epoch = 0
|
|
18
|
+
private stale = true
|
|
19
|
+
private disposed = false
|
|
20
|
+
|
|
21
|
+
/** @public @author ddj 2026年10月20号 @param fetchStatus Authenticated read-only query. @param page Visibility boundary, omitted for server tests. */
|
|
22
|
+
constructor(private readonly fetchStatus: FetchStatus, private readonly page?: Document) {}
|
|
23
|
+
|
|
24
|
+
/** @public @author ddj 2026年10月20号 @param sessionId Row identity. @returns Stable view until the next observation. */
|
|
25
|
+
get(sessionId: string): SessionMcpView { return this.views.get(sessionId) ?? (this.stale ? unavailable : inactive) }
|
|
26
|
+
|
|
27
|
+
/** @public @author ddj 2026年10月20号 @param sessionId Row identity. @param listener React invalidation. @returns Idempotent unsubscribe; the last consumer stops polling. */
|
|
28
|
+
subscribe(sessionId: string, listener: () => void): () => void {
|
|
29
|
+
if (this.disposed) return () => undefined
|
|
30
|
+
const first = this.listeners.size === 0
|
|
31
|
+
const listeners = this.listeners.get(sessionId) ?? new Set()
|
|
32
|
+
listeners.add(listener)
|
|
33
|
+
this.listeners.set(sessionId, listeners)
|
|
34
|
+
if (first) {
|
|
35
|
+
this.page?.addEventListener('visibilitychange', this.visibility)
|
|
36
|
+
void this.poll()
|
|
37
|
+
}
|
|
38
|
+
return () => {
|
|
39
|
+
listeners.delete(listener)
|
|
40
|
+
if (listeners.size === 0) this.listeners.delete(sessionId)
|
|
41
|
+
if (this.listeners.size === 0) this.pause()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @private @author ddj 2026年10月20号 Stops pending work when hidden; resuming never accepts an old response. */
|
|
46
|
+
private visibility = (): void => {
|
|
47
|
+
if (this.page?.hidden) this.invalidate()
|
|
48
|
+
else void this.poll()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @private @author ddj 2026年10月20号 Invalidates responses without allowing overlapping transport requests. */
|
|
52
|
+
private invalidate(): void {
|
|
53
|
+
this.epoch += 1
|
|
54
|
+
clearTimeout(this.timer)
|
|
55
|
+
this.timer = undefined
|
|
56
|
+
this.request?.abort()
|
|
57
|
+
this.publish(undefined)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @private @author ddj 2026年10月20号 Cleans listener lifetime as well as timers. */
|
|
61
|
+
private pause(): void {
|
|
62
|
+
this.page?.removeEventListener('visibilitychange', this.visibility)
|
|
63
|
+
this.invalidate()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @public @author ddj 2026年10月20号 Permanently disposes HMR-owned polling. */
|
|
67
|
+
dispose(): void { this.disposed = true; this.pause(); this.listeners.clear(); this.views.clear() }
|
|
68
|
+
|
|
69
|
+
/** @private @author ddj 2026年10月20号 @param snapshot Fresh ledger, or undefined to mark all previous observations stale. */
|
|
70
|
+
private publish(snapshot?: Snapshot): void {
|
|
71
|
+
this.stale = snapshot === undefined
|
|
72
|
+
if (snapshot) this.views = new Map(snapshot.sessions.map((status) => [status.sessionId, { status, stale: false }]))
|
|
73
|
+
else for (const [id, view] of this.views) this.views.set(id, { ...view, stale: true })
|
|
74
|
+
for (const listeners of this.listeners.values()) for (const listener of listeners) listener()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @private @author ddj 2026年10月20号 Queries once, with a 10-second deadline and a 2.5-second gap after settlement. */
|
|
78
|
+
private async poll(): Promise<void> {
|
|
79
|
+
if (this.disposed || this.request || !this.listeners.size || this.page?.hidden) return
|
|
80
|
+
const request = new AbortController()
|
|
81
|
+
const epoch = this.epoch
|
|
82
|
+
this.request = request
|
|
83
|
+
/** @author ddj 2026年10月20号 Marks overdue responses unusable even if a transport ignores cancellation. */
|
|
84
|
+
const expire = (): void => { request.abort(); if (epoch === this.epoch) this.publish(undefined) }
|
|
85
|
+
const deadline = setTimeout(expire, 10000)
|
|
86
|
+
try {
|
|
87
|
+
const snapshot = await this.fetchStatus(request.signal)
|
|
88
|
+
if (epoch === this.epoch && !request.signal.aborted) this.publish(snapshot)
|
|
89
|
+
} catch {
|
|
90
|
+
if (epoch === this.epoch) this.publish(undefined)
|
|
91
|
+
} finally {
|
|
92
|
+
clearTimeout(deadline)
|
|
93
|
+
this.request = undefined
|
|
94
|
+
if (!this.disposed && this.listeners.size && !this.page?.hidden) this.timer = setTimeout(() => { void this.poll() }, 2500)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** @public @author ddj 2026年10月20号 @returns Browser-owned singleton per plugin apply; legacy Hosts fail safely as stale. */
|
|
100
|
+
export function mcpSessionState(): McpSessionState {
|
|
101
|
+
/** @author ddj 2026年10月20号 @param signal Fetch lifetime. @returns Current in-memory host ledger. @throws RPC errors. */
|
|
102
|
+
const fetchStatus = async (signal: AbortSignal): Promise<Snapshot> => {
|
|
103
|
+
const result = await rpc('mcp.sessionSnapshot', {}, signal)
|
|
104
|
+
if (!result.ok) throw new Error('MCP 状态暂不可用')
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
107
|
+
return new McpSessionState(fetchStatus, document)
|
|
108
|
+
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* 交互层:edrv.goToDefinition(F12)/ edrv.findReferences(Shift+F12 / 右键)均走 Monaco 原生命令;
|
|
6
6
|
* Ctrl+点击使用处交给原生;Lua 表枚举字段声明处改为打开引用 Peek,
|
|
7
7
|
* 数据经 host 的 LSP RPC 提供(registerEditorOpener 接管 edrv:// 点击跳转)。
|
|
8
|
-
*
|
|
8
|
+
* 定义结果返回前预热目标 model,避免 hover 定义链接预览抛 "Model not found"。
|
|
9
|
+
* 作者 ddj 2026-08-27 / 2026-09-11 / 2026-10-20
|
|
9
10
|
*/
|
|
10
11
|
import {
|
|
11
12
|
pathOfModel, syncDoc, closeDoc, findDefinition, findReferences,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
LSP_COMPLETION_KIND_NAMES, LSP_INSERT_TEXT_FORMAT_SNIPPET,
|
|
20
21
|
} from '../../../shared/lsp.js'
|
|
21
22
|
import { luaKeywordsAt, luaTypesAt, mergeKeywords } from './keywords.js'
|
|
23
|
+
import { warmModelFor } from '../../state/modelWarmer.js'
|
|
22
24
|
|
|
23
25
|
const LSP_LANGS = ['lua', 'csharp']
|
|
24
26
|
|
|
@@ -79,6 +81,31 @@ export function navTargetOf(selectionOrPosition) {
|
|
|
79
81
|
return endColumn === undefined ? { line, column, endLine } : { line, column, endLine, endColumn }
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
/**
|
|
85
|
+
* 预热定义目标文件的 Monaco model(供 Ctrl+hover 定义链接预览使用)。
|
|
86
|
+
*
|
|
87
|
+
* 为什么需要:Monaco 的 GotoDefinitionAtPositionEditorContribution 在渲染定义链接预览时,
|
|
88
|
+
* 对目标 uri 调 `createModelReference` 后**只挂 then、没有 catch**;本插件的定义目标是
|
|
89
|
+
* 自造的 `edrv://` uri,目标文件若从未打开过就没有已注册 model,`createModelReference`
|
|
90
|
+
* 会 reject("Model not found") 变成未捕获的 promise 异常(弹到 Console)。
|
|
91
|
+
* 本 vendored Monaco 也没有 `registerTextModelContentProvider` 可惰性喂内容,故只能提前建模型。
|
|
92
|
+
*
|
|
93
|
+
* 只预热单一定义:多定义分支(`locations.length > 1`)Monaco 走列表、不调
|
|
94
|
+
* `createModelReference`,没必要读盘。预热纯属增强,任何异常都在此吞掉,
|
|
95
|
+
* 不得让跳转查询失败(F12 与 Ctrl+点击不依赖模型是否已建)。
|
|
96
|
+
* @author ddj 2026年10月20号
|
|
97
|
+
* @param locations findDefinition 的客户端结果(含 edrv uri)
|
|
98
|
+
*/
|
|
99
|
+
async function prewarmDefinitionTarget(locations) {
|
|
100
|
+
if (!Array.isArray(locations) || locations.length !== 1) return
|
|
101
|
+
try {
|
|
102
|
+
const target = lspUriToAbs(locations[0]?.uri)
|
|
103
|
+
if (target) await warmModelFor(target)
|
|
104
|
+
} catch (error) {
|
|
105
|
+
/* 预热失败静默降级:等价于本次 hover 不显示预览 */
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
82
109
|
/** 注册全部 Monaco LSP provider 与文档跟踪(幂等)。 */
|
|
83
110
|
export function registerLspProviders(monaco) {
|
|
84
111
|
if (registered) return
|
|
@@ -151,10 +178,12 @@ export function registerLspProviders(monaco) {
|
|
|
151
178
|
},
|
|
152
179
|
}),
|
|
153
180
|
monaco.languages.registerDefinitionProvider(LSP_LANGS, {
|
|
154
|
-
provideDefinition: (model, position, token) => {
|
|
181
|
+
provideDefinition: async (model, position, token) => {
|
|
155
182
|
const path = pathOfModel(model)
|
|
156
183
|
if (!path) return []
|
|
157
|
-
|
|
184
|
+
const locations = await findDefinition(path, model.getValue(), position)
|
|
185
|
+
await prewarmDefinitionTarget(locations)
|
|
186
|
+
return locations
|
|
158
187
|
},
|
|
159
188
|
}),
|
|
160
189
|
monaco.languages.registerReferenceProvider(LSP_LANGS, {
|
|
@@ -286,8 +286,11 @@ export function installOfficial(options: {
|
|
|
286
286
|
slots: SlotsLike
|
|
287
287
|
renderTab: unknown
|
|
288
288
|
guide: { title: () => string; description: () => string }
|
|
289
|
+
/** Tab 标题 thunk(缺省用内置中文常量;传入后由调用方按当前语言解析)。 */
|
|
290
|
+
title?: () => string
|
|
289
291
|
}): (() => void) | null {
|
|
290
292
|
const { tabs, service, slots, renderTab, guide } = options
|
|
293
|
+
const titleOf = typeof options.title === 'function' ? options.title : () => OFFICIAL_TAB_TITLE
|
|
291
294
|
const open = (path: string | null, focusDiff: boolean): boolean => {
|
|
292
295
|
try {
|
|
293
296
|
// 官方语义:打开即展开侧栏;seat 未挂载(无会话画面)时抛错,降级其他打开器
|
|
@@ -305,7 +308,7 @@ export function installOfficial(options: {
|
|
|
305
308
|
id: OFFICIAL_TAB_ID,
|
|
306
309
|
kind: OFFICIAL_TAB_KIND,
|
|
307
310
|
priority: 'extension',
|
|
308
|
-
title: () =>
|
|
311
|
+
title: () => titleOf(),
|
|
309
312
|
guide: [{ order: 20, title: guide.title, description: guide.description }],
|
|
310
313
|
})
|
|
311
314
|
} catch (error) {
|