dsh-vscode-mode 0.12.0 → 0.13.1

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.
Files changed (71) hide show
  1. package/README.md +148 -112
  2. package/assets/shell/dsh-open.cs +65 -149
  3. package/assets/shell/dsh-open.mjs +241 -0
  4. package/assets/shell/dsh-open.ps1 +28 -47
  5. package/assets/shell/dsh-open.sh +25 -66
  6. package/lib/client.js +30185 -29711
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +3889 -2336
  9. package/lib/index.js.map +1 -1
  10. package/package.json +9 -8
  11. package/skills/dsh-vscodemode-mcp/SKILL.md +94 -132
  12. package/src/childEnv.ts +19 -0
  13. package/src/client/addToConversation.ts +4 -2
  14. package/src/client/ai/inlineProvider.ts +1 -8
  15. package/src/client/commandBridge.ts +6 -2
  16. package/src/client/externalOpen.ts +40 -19
  17. package/src/client/index.ts +17 -15
  18. package/src/client/keybindings.ts +18 -1
  19. package/src/client/monaco/loader.ts +3 -1
  20. package/src/client/openFlow.ts +36 -28
  21. package/src/client/openReceipt.ts +64 -0
  22. package/src/client/panelCommands.ts +101 -0
  23. package/src/client/pdf/pdfLoader.ts +4 -2
  24. package/src/client/receiptState.ts +120 -0
  25. package/src/client/rpc.ts +30 -4
  26. package/src/client/shortcutsOfficial.ts +39 -4
  27. package/src/client/sidebarBridge.ts +7 -0
  28. package/src/client/styles/mcp.css +1 -19
  29. package/src/client/ui/DiffBadge.ts +2 -1
  30. package/src/client/ui/EditorView.ts +237 -55
  31. package/src/client/ui/McpSettings.ts +32 -7
  32. package/src/client/ui/OfficialSideTab.ts +5 -2
  33. package/src/client/ui/QuickOpen.ts +10 -9
  34. package/src/client/ui/commandCatalog.ts +5 -4
  35. package/src/client/useOpenReceipt.ts +128 -0
  36. package/src/compat.ts +5 -3
  37. package/src/dap/manager.ts +46 -13
  38. package/src/devForm.ts +252 -92
  39. package/src/externalHandoff.ts +93 -52
  40. package/src/fileOpenSettings.ts +1 -24
  41. package/src/hostImport.ts +19 -0
  42. package/src/httpGuard.ts +101 -0
  43. package/src/index.ts +106 -97
  44. package/src/integrate.ts +56 -50
  45. package/src/lsp/transport.ts +84 -127
  46. package/src/mcp.ts +58 -9
  47. package/src/mcpIsolation.ts +211 -102
  48. package/src/mcpProject.ts +220 -133
  49. package/src/mcpRuntime.ts +343 -0
  50. package/src/openBridge.ts +69 -0
  51. package/src/openInbox.ts +207 -0
  52. package/src/paths.ts +8 -5
  53. package/src/processTree.ts +63 -0
  54. package/src/reveal.ts +5 -3
  55. package/src/routes.ts +156 -154
  56. package/src/rpc.ts +14 -13
  57. package/src/shared/appUrl.ts +19 -0
  58. package/src/shared/externalOpen.ts +71 -0
  59. package/src/shared/integration.ts +2 -0
  60. package/src/shared/keybindings.ts +8 -5
  61. package/src/shared/mcp.ts +4 -1
  62. package/src/shared/rpc.ts +7 -4
  63. package/src/shared/svn.ts +6 -8
  64. package/src/svnPlanTool.ts +51 -0
  65. package/src/unityBridge.ts +26 -3
  66. package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
  67. package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
  68. package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
  69. package/unity/com.dsh.editor/README.md +16 -25
  70. package/unity/com.dsh.editor/package.json +2 -2
  71. package/src/client/ui/ShortcutSettings.ts +0 -294
@@ -1,155 +1,264 @@
1
- /**
2
- * dsh-vscode-mode host — 项目 MCP 的 agent 级隔离。
3
- * 项目 MCP 连接仍由 Host 维护,但每个 agent 只继承当前 workspace 的项目工具;
4
- * tools.restrict() 负责模型可见性,tools.guard() 负责执行层兜底拒绝。
5
- * 作者 ddj 2026年08月22号
6
- */
1
+ /** Project MCP visibility, instruction shadows, and execution guards. @author ddj 2026年09月28号 */
2
+ import { posix, win32 } from 'node:path'
7
3
  import { LEGACY_PROJECT_PREFIX, entryHash, isProjectEntryId } from './compat.js'
8
4
  import { entriesOf } from './mcp.js'
9
5
  import { hashWorkspace } from './mcpProject.js'
6
+ import { runtimeOf } from './mcpRuntime.js'
10
7
  import type { Ctx } from './store.js'
11
- import { log } from './log.js'
12
8
 
13
9
  const UNKNOWN_PROJECT = '__unknown_project__'
14
-
10
+ const RESOURCE_TOOLS = new Set(['list_mcp_resources', 'list_mcp_resource_templates', 'read_mcp_resource'])
15
11
  type ToolProjects = Map<string, string>
16
- type AgentState = { dispose?: () => void; denyKey: string }
12
+ type AgentState = { dispose?: () => void; denyKey: string; shadows: Map<string, () => void> }
17
13
 
18
- /** 统一 workspace 路径,供 cwd 父路径匹配。 */
14
+ /**
15
+ * Normalize path separators and Windows case while retaining POSIX case sensitivity.
16
+ * @public @author ddj 2026年09月28号
17
+ * @param path Workspace or agent cwd.
18
+ * @returns Comparable path with a stable root representation.
19
+ */
19
20
  export function normalizePath(path: string): string {
20
- const value = String(path ?? '').replace(/\\/g, '/').replace(/\/+/g, '/')
21
- return value.replace(/\/$/, '').toLowerCase()
21
+ const raw = String(path ?? '').replace(/\\/g, '/')
22
+ const windows = /^[a-z]:/i.test(raw) || raw.startsWith('//')
23
+ const normalized = (windows ? win32.normalize(raw) : posix.normalize(raw)).replace(/\\/g, '/')
24
+ const value = normalized.replace(/\/+$/, '') || '/'
25
+ return windows ? value.toLowerCase() : value
22
26
  }
23
27
 
24
- /** 在已注册 workspace 中匹配 cwd,优先最长父路径。 */
28
+ /**
29
+ * Match the most specific registered parent workspace.
30
+ * @public @author ddj 2026年09月28号
31
+ * @param cwd Agent working directory.
32
+ * @param paths Registered workspace paths.
33
+ * @returns Matching original path, or undefined.
34
+ */
25
35
  export function matchWorkspace(cwd: string | undefined, paths: string[]): string | undefined {
26
36
  if (!cwd) return undefined
27
37
  const target = normalizePath(cwd)
28
38
  const matches = paths.filter((path) => {
29
39
  const root = normalizePath(path)
30
- return target === root || target.startsWith(root + '/')
40
+ return target === root || target.startsWith(root === '/' ? root : root + '/')
31
41
  })
32
42
  matches.sort((a, b) => normalizePath(b).length - normalizePath(a).length)
33
43
  return matches[0]
34
44
  }
35
45
 
36
- /** 根据 entry id 判断是否为新旧格式项目 MCP(兼容层统一判定)。 */
37
46
  export const isProjectEntry = isProjectEntryId
38
-
39
- /** 兼容:旧版前缀常量名(mcpIsolation 历史导出,语义不变)。 */
40
47
  export const LEGACY_PREFIX = LEGACY_PROJECT_PREFIX
41
48
 
42
- /** 返回一个 agent 不应继承的项目工具名。 */
49
+ /**
50
+ * Select foreign project tool names.
51
+ * @public @author ddj 2026年09月28号
52
+ * @param toolProjects Tool ownership map.
53
+ * @param currentPath Matching workspace.
54
+ * @returns Sorted deny list.
55
+ */
43
56
  export function denyTools(toolProjects: ToolProjects, currentPath: string | undefined): string[] {
44
- return [...toolProjects.entries()].filter(([, owner]) => owner !== currentPath).map(([name]) => name).sort()
57
+ return [...toolProjects].filter(([, owner]) => owner !== currentPath).map(([name]) => name).sort()
45
58
  }
46
59
 
47
- /** 从 loader entry 和工具注册表建立工具到项目路径的映射。 */
48
- function projectTools(ctx: Ctx): ToolProjects {
49
- const result: ToolProjects = new Map()
50
- const registry = ctx.get('workspaceRegistry')
51
- const workspaces = registry?.list?.() ?? []
52
- const byHash: Map<string, string> = new Map(workspaces.map((ws: { path: string }) => [hashWorkspace(ws.path), ws.path] as [string, string]))
53
- const visible = ctx.get('tools')?.view?.(void 0)?.visible
54
- if (!(visible instanceof Map)) return result
60
+ /**
61
+ * Include historical global entries in ownership until migration has disposed them.
62
+ * @public @author ddj 2026年09月28号
63
+ * @param ctx Host services.
64
+ * @returns Server-to-workspace ownership map.
65
+ */
66
+ export function serverOwners(ctx: Ctx): Map<string, string> {
67
+ const result = runtimeOf(ctx).owners()
68
+ const workspaces = ctx.get('workspaceRegistry')?.list?.() ?? []
69
+ const hashes = new Map(workspaces.map((ws: { path: string }) => [hashWorkspace(ws.path), ws.path]))
55
70
  for (const entry of entriesOf(ctx)) {
56
71
  const id = String(entry.id ?? entry.options?.id ?? '')
57
- if (!isProjectEntry(id)) continue
58
- const hash = entryHash(id)
59
- const owner = hash ? byHash.get(hash) ?? UNKNOWN_PROJECT : UNKNOWN_PROJECT
60
- const serverName = entry.options?.config?.serverName
61
- if (typeof serverName !== 'string') continue
62
- const prefix = `mcp__${serverName}__`
63
- for (const name of visible.keys()) if (String(name).startsWith(prefix)) result.set(String(name), owner)
72
+ const name = entry.options?.config?.serverName
73
+ if (!isProjectEntry(id) || typeof name !== 'string') continue
74
+ result.set(name, String(hashes.get(entryHash(id)) ?? UNKNOWN_PROJECT))
64
75
  }
65
76
  return result
66
77
  }
67
78
 
68
- /** 取得 agent 当前 cwd 所属 workspace。 */
69
- function agentWorkspace(ctx: Ctx, agent: any): string | undefined {
70
- const cwd = agent?.session?.header?.cwd
71
- const paths = (ctx.get('workspaceRegistry')?.list?.() ?? []).map((ws: { path: string }) => ws.path)
72
- return matchWorkspace(cwd, paths)
79
+ /**
80
+ * Resolve managed server attribution for namespaced tools and shared resource calls.
81
+ * @public @author ddj 2026年09月28号
82
+ * @param exec Tool execution with arguments.server for shared resource tools.
83
+ * @returns Attributed server name, or undefined for unrelated tools.
84
+ */
85
+ export function callServer(exec: any): string | undefined {
86
+ const name = String(exec?.name ?? '')
87
+ if (RESOURCE_TOOLS.has(name)) return typeof exec?.arguments?.server === 'string' ? exec.arguments.server : undefined
88
+ return /^mcp__(.+?)__/.exec(name)?.[1]
73
89
  }
74
90
 
75
- /** 同步一个 agent 的项目工具 deny 列表。 */
76
- function syncAgent(ctx: Ctx, state: AgentState, agent: any, tools: ToolProjects): void {
77
- const current = agentWorkspace(ctx, agent)
78
- const denied = denyTools(tools, current)
79
- const nextKey = denied.join('\n')
80
- if (state.denyKey === nextKey) return
91
+ /**
92
+ * Check workspace ownership before any tool or resource network operation.
93
+ * @public @author ddj 2026年09月28号
94
+ * @param ctx Host services.
95
+ * @param exec Caller and tool arguments.
96
+ * @returns A denial message, or undefined for an allowed/global call.
97
+ */
98
+ export function guardMcp(ctx: Ctx, exec: any): string | undefined {
99
+ const name = String(exec?.name ?? '')
100
+ if (!RESOURCE_TOOLS.has(name) && !name.startsWith('mcp__')) return undefined
101
+ const server = callServer(exec)
102
+ const owners = serverOwners(ctx)
103
+ const current = runtimeOf(ctx).workspace(exec.agent)
104
+ const owned = RESOURCE_TOOLS.has(name) ? [...owners].filter(([key]) => key === server)
105
+ : [...owners].filter(([key]) => name.startsWith('mcp__' + key + '__'))
106
+ if (!owned.some(([, owner]) => owner !== current)) return undefined
107
+ return current === undefined ? '项目 MCP 需要在已注册工作区的对话中使用' : '已拒绝:当前对话工作区不能使用其他项目的 MCP'
108
+ }
109
+
110
+ /**
111
+ * Restrict inherited names using the public restrictableNames set, even after already hidden.
112
+ * @private @author ddj 2026年09月28号
113
+ * @param ctx Host services.
114
+ * @param agent Target scope.
115
+ * @param state Owned restriction disposers.
116
+ * @param owners Server ownership.
117
+ * @sideEffects Replaces only this plugin's scoped restriction.
118
+ */
119
+ function syncTools(ctx: Ctx, agent: any, state: AgentState, owners: Map<string, string>): void {
120
+ const tools = agent.ctx.get('tools')
121
+ const view = tools?.view?.(agent)
122
+ const names: Iterable<string> = view?.restrictableNames ?? view?.knownNames ?? view?.visible?.keys?.() ?? []
123
+ const current = runtimeOf(ctx).workspace(agent)
124
+ const denied = [...names].filter((name) => {
125
+ return [...owners].some(([server, owner]) => name.startsWith('mcp__' + server + '__') && owner !== current)
126
+ }).sort()
127
+ const key = denied.join('\n')
128
+ if (key === state.denyKey) return
129
+ state.denyKey = key
130
+ let dispose: (() => void) | undefined
131
+ try { dispose = denied.length > 0 ? tools.restrict({ deny: denied }) : undefined }
132
+ catch (error) { state.denyKey = ''; throw error }
81
133
  state.dispose?.()
82
- state.dispose = undefined
83
- if (denied.length > 0) {
84
- try {
85
- state.dispose = agent.ctx.tools.restrict({ deny: denied })
86
- } catch (error) {
87
- log.warn('MCP agent restriction failed: ' + String(error))
88
- return
89
- }
134
+ state.dispose = dispose
135
+ }
136
+
137
+ /**
138
+ * Filter the official assembled server list, retaining unmanaged and resource-only providers.
139
+ * @public @author ddj 2026年09月28号
140
+ * @param ctx Host services.
141
+ * @param agent Actual assembly scope, including a descendant of the listening scope.
142
+ * @param text Official mcp-resource-servers section text.
143
+ * @returns Scope-visible resource help; unfamiliar list formats fail closed for foreign projects.
144
+ */
145
+ export function filterResources(ctx: Ctx, agent: any, text: string): string {
146
+ const current = runtimeOf(ctx).workspace(agent)
147
+ const owners = serverOwners(ctx)
148
+ const foreign = new Set([...owners].filter(([, owner]) => owner !== current).map(([name]) => name))
149
+ if (!foreign.size || !text) return text
150
+ const match = /(\[[^\n]*\])\.\s*$/.exec(text)
151
+ if (!match) return ''
152
+ let names: unknown
153
+ try { names = JSON.parse(match[1]) } catch { return '' }
154
+ if (!Array.isArray(names) || names.some((name) => typeof name !== 'string')) return ''
155
+ const visible = names.filter((name) => !foreign.has(name))
156
+ return visible.length ? text.slice(0, match.index) + JSON.stringify(visible) + '.' : ''
157
+ }
158
+
159
+ /**
160
+ * Apply scope-specific resource visibility to the public assembled prompt snapshot.
161
+ * @private @author ddj 2026年09月28号
162
+ * @param ctx Host services.
163
+ * @param assembly Downstream assembly result.
164
+ * @param agent Actual viewing scope.
165
+ * @returns Detached section list; all unrelated sections retain their original identity.
166
+ */
167
+ function filterAssembly(ctx: Ctx, assembly: any, agent: any): any {
168
+ const sections = assembly.sections.map((section: any) => section.name === 'mcp-resource-servers'
169
+ ? { ...section, text: filterResources(ctx, agent, section.text) } : section)
170
+ return { ...assembly, sections }
171
+ }
172
+
173
+ /**
174
+ * Shadow inherited foreign instructions without changing the scope's parent chain.
175
+ * @private @author ddj 2026年09月28号
176
+ * @param ctx Host services.
177
+ * @param agent Target scope.
178
+ * @param state Owned prompt registrations.
179
+ * @param owners Current project server ownership.
180
+ * @sideEffects Registers blank foreign instruction sections in the exact agent scope.
181
+ */
182
+ function syncPrompts(ctx: Ctx, agent: any, state: AgentState, owners: Map<string, string>): void {
183
+ const prompt = agent.ctx.get('systemPrompt')
184
+ if (!prompt) return
185
+ const current = runtimeOf(ctx).workspace(agent)
186
+ const foreign = new Set([...owners].filter(([, path]) => path !== current).map(([name]) => name))
187
+ for (const [name, dispose] of state.shadows) {
188
+ if (foreign.has(name)) continue
189
+ dispose()
190
+ state.shadows.delete(name)
191
+ }
192
+ const order = prompt.getSectionOrder('MCP_SERVERS')
193
+ for (const name of foreign) {
194
+ if (state.shadows.has(name)) continue
195
+ state.shadows.set(name, prompt.section({ name: 'mcp:' + name, order, interpolate: false, text: '' }))
90
196
  }
91
- state.denyKey = nextKey
92
197
  }
93
198
 
94
- /** 为当前 Host 安装项目 MCP 的 agent restriction 与执行 guard。 */
199
+ /**
200
+ * Dispose only isolation effects owned for one agent.
201
+ * @private @author ddj 2026年09月28号
202
+ * @param state Stored effect handles.
203
+ * @sideEffects Removes restrictions, blank instruction shadows, and resource help override.
204
+ */
205
+ function clearState(state: AgentState): void {
206
+ state.dispose?.()
207
+ for (const dispose of state.shadows.values()) dispose()
208
+ state.shadows.clear()
209
+ }
210
+
211
+ /**
212
+ * Install synchronous creation isolation and guards before installing the MCP runtime.
213
+ * @public @author ddj 2026年09月28号
214
+ * @param ctx Host lifecycle context.
215
+ * @sideEffects Owns per-agent restrictions and prompt shadows plus a global execution guard.
216
+ */
95
217
  export function installIsolation(ctx: Ctx): void {
96
218
  const tools = ctx.get('tools')
97
219
  const agents = ctx.get('agents')
98
220
  if (!tools || !agents) return
99
221
  const states = new Map<any, AgentState>()
100
- let toolProjects = projectTools(ctx)
101
- let scheduled = false
102
222
  let syncing = false
103
-
223
+ const syncAgent = (agent: any) => {
224
+ const state = states.get(agent) ?? { denyKey: '', shadows: new Map() }
225
+ states.set(agent, state)
226
+ const owners = serverOwners(ctx)
227
+ syncTools(ctx, agent, state, owners)
228
+ syncPrompts(ctx, agent, state, owners)
229
+ }
104
230
  const syncAll = () => {
105
231
  if (syncing) return
106
232
  syncing = true
107
- try {
108
- toolProjects = projectTools(ctx)
109
- for (const agent of agents.list?.() ?? []) {
110
- const state = states.get(agent) ?? { denyKey: '' }
111
- states.set(agent, state)
112
- syncAgent(ctx, state, agent, toolProjects)
113
- }
114
- } finally {
115
- syncing = false
116
- }
117
- }
118
- const scheduleSync = () => {
119
- if (scheduled) return
120
- scheduled = true
121
- queueMicrotask(() => {
122
- scheduled = false
123
- syncAll()
124
- })
233
+ try { for (const agent of agents.list?.() ?? []) syncAgent(agent) }
234
+ finally { syncing = false }
125
235
  }
126
- const onCreated = ctx.on('agent/created', ({ agent }: { agent: any }) => {
127
- const state = { denyKey: '' }
128
- states.set(agent, state)
129
- syncAgent(ctx, state, agent, toolProjects)
130
- })
131
- const onDisposed = ctx.on('agent/disposed', ({ agent }: { agent: any }) => {
236
+ const onCreated = ({ agent }: { agent: any }) => { syncAgent(agent) }
237
+ const onDisposed = ({ agent }: { agent: any }) => {
132
238
  const state = states.get(agent)
133
- state?.dispose?.()
239
+ if (state) clearState(state)
134
240
  states.delete(agent)
135
- })
136
- const onToolsChange = ctx.on('tools/change', scheduleSync)
137
- const stopGuard = tools.guard((exec: any) => {
138
- const owner = toolProjects.get(String(exec?.name ?? ''))
139
- if (!owner) return undefined
140
- const current = agentWorkspace(ctx, exec.agent)
141
- if (owner === current) return undefined
142
- return current === undefined
143
- ? '项目 MCP 需要在已注册工作区的对话中使用'
144
- : '已拒绝:当前对话工作区不能使用其他项目的 MCP'
145
- })
146
- syncAll()
147
- ctx.effect(() => () => {
148
- onCreated?.()
149
- onDisposed?.()
150
- onToolsChange?.()
151
- stopGuard?.()
152
- for (const state of states.values()) state.dispose?.()
241
+ }
242
+ const guard = (exec: any) => guardMcp(ctx, exec)
243
+ /**
244
+ * Filter the completed assembly using its actual viewing scope.
245
+ * @private @author ddj 2026年09月28号
246
+ * @param assembly Initial prompt snapshot.
247
+ * @param context Assembly scope.
248
+ * @param next Downstream assembly continuation.
249
+ * @returns Completed prompt with foreign project resource names removed.
250
+ */
251
+ const filterPrompt = async (assembly: any, context: any, next: () => Promise<any>) => {
252
+ return filterAssembly(ctx, await next(), context.scope)
253
+ }
254
+ const disposers = [ctx.on('agent/created', onCreated), ctx.on('agent/disposed', onDisposed),
255
+ ctx.on('tools/change', syncAll), ctx.on('vscode-mode/mcp-change', syncAll),
256
+ ctx.on('system-prompt/assemble', filterPrompt), tools.guard(guard)]
257
+ const cleanup = () => {
258
+ for (const dispose of disposers) dispose?.()
259
+ for (const state of states.values()) clearState(state)
153
260
  states.clear()
154
- }, 'vscode-mode:mcp-isolation')
261
+ }
262
+ ctx.effect(() => cleanup, 'vscode-mode:mcp-isolation')
263
+ syncAll()
155
264
  }