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
@@ -165,8 +165,9 @@ export function chordToBinding(chord: string): OfficialBinding | null {
165
165
  }
166
166
 
167
167
  /**
168
- * 官方绑定 → 插件显示键位弦(primary 显示为 Ctrl,与历史 tooltip 文案保持一致)。
169
- * @author ddj 2026年10月
168
+ * 官方绑定 → 可读键位弦,保留规范化后的 Control/Meta 物理区别。
169
+ * 官方目录展示优先使用官方 keys;此函数仅作为缺少展示字段时的回落。
170
+ * @author ddj 2026年09月28号
170
171
  * @param binding 官方绑定
171
172
  * @returns 插件弦(不可表示 → null)
172
173
  */
@@ -181,7 +182,8 @@ export function bindingToChord(binding: OfficialBinding): string | null {
181
182
  suffix = '+' + secondKey
182
183
  }
183
184
  const parts: string[] = []
184
- if (binding.modifiers.includes('primary')) parts.push('Ctrl')
185
+ if (binding.modifiers.includes('primary') || binding.modifiers.includes('control')) parts.push('Ctrl')
186
+ if (binding.modifiers.includes('meta')) parts.push('Meta')
185
187
  if (binding.modifiers.includes('shift')) parts.push('Shift')
186
188
  if (binding.modifiers.includes('alt')) parts.push('Alt')
187
189
  return [...parts, key + suffix].join('+')
@@ -217,12 +219,13 @@ export const SHORTCUT_PROFILES: Record<string, ShortcutProfiles> = {
217
219
  'web:macos': bind('KeyP', ['primary', 'alt', 'shift']),
218
220
  'web:windows': bind('KeyP', ['primary', 'alt', 'shift']),
219
221
  },
222
+ // 官方 session.fork 占用两种 Web primary+shift+F;加 Alt 避开联合目录冲突。
220
223
  'edrv.searchInFiles': {
221
224
  'desktop:macos': bind('KeyF', ['primary', 'shift']),
222
225
  'desktop:windows': bind('KeyF', ['primary', 'shift']),
223
226
  'desktop:linux': bind('KeyF', ['primary', 'shift']),
224
- 'web:macos': bind('KeyF', ['primary', 'shift']),
225
- 'web:windows': bind('KeyF', ['primary', 'shift']),
227
+ 'web:macos': bind('KeyF', ['primary', 'alt', 'shift']),
228
+ 'web:windows': bind('KeyF', ['primary', 'alt', 'shift']),
226
229
  },
227
230
  // Ctrl+Shift+V(primary+KeyV)在 linux 为保留键(官方 bindingIssue 不豁免 shift),desktop:linux 不声明
228
231
  'edrv.toggleMarkdownPreview': {
package/src/shared/mcp.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /** dsh-vscode-mode MCP 管理共享数据契约。作者 ddj 2026年08月22号 */
2
2
 
3
3
  export type MpcTransport = 'stdio' | 'streamable-http'
4
- export type MpcStatus = 'connected' | 'connecting' | 'error' | 'disabled'
4
+ export type MpcStatus = 'connected' | 'connecting' | 'error' | 'disabled' | 'configured' | 'unverified'
5
5
 
6
6
  export interface MpcTool {
7
7
  name: string
@@ -18,6 +18,7 @@ export interface MpcConfig {
18
18
  url?: string
19
19
  headers?: Record<string, string>
20
20
  toolCallTimeoutMs?: number
21
+ maxInstructionBytes?: number
21
22
  failOnStartupError?: boolean
22
23
  reconnect?: Record<string, unknown>
23
24
  }
@@ -29,6 +30,8 @@ export interface MpcServer {
29
30
  transport: MpcTransport
30
31
  config: MpcConfig
31
32
  status: MpcStatus
33
+ /** Mounted official plugin fibers, not verified live connections. */
34
+ instanceCount?: number
32
35
  toolCount: number
33
36
  tools: MpcTool[]
34
37
  error?: string
package/src/shared/rpc.ts CHANGED
@@ -22,6 +22,7 @@ import type { AiConfigPatch, AiConfigView, AiDirectoryView, AiInlineRequest, AiI
22
22
  import type { SvnAction, SvnAiPlan, SvnChangeEntry, SvnConflictArtifact, SvnDiffRevResult, SvnIgnoreItem, SvnLogEntry, SvnRemoteOutdatedEntry, SvnStatusPayload, SvnSumEntry, SvnUpdatePoll, SvnUpdateResult, SvnUpdateState } from './svn.js'
23
23
  import type { DapAction, DapAdapterInfo, DapBreakpointAck, DapBreakpointInput, DapConfigSnippet, DapConfigSource, DapDebugConfig, DapFrameView, DapPhase, DapPollResult, DapProcessInfo, DapScopeView, DapVariableView } from './dap.js'
24
24
  import { DAP_ACTIONS } from './dap.js'
25
+ import { appUrl } from './appUrl.js'
25
26
  import type { LoggerLevel } from './logger.js'
26
27
 
27
28
  /** webServer 精确路由。 */
@@ -133,7 +134,7 @@ export async function readBinaryPreview(
133
134
  */
134
135
  async function tryBinaryRead(args: RpcRequestMap['edrv.readBinary']): Promise<PreviewRead | null> {
135
136
  try {
136
- const res = await fetch(RPC_PATH, {
137
+ const res = await fetch(appUrl(RPC_PATH), {
137
138
  method: 'POST',
138
139
  headers: { 'content-type': 'application/json' },
139
140
  body: JSON.stringify({ method: 'edrv.readBinary', args }),
@@ -382,7 +383,8 @@ export interface RpcRequestMap {
382
383
  'edrv.unity.install': { path: string }
383
384
  'edrv.externalStat': { path: string }
384
385
  'edrv.external.handoff': { paths: string[]; line?: number; column?: number }
385
- 'edrv.external.pending': {}
386
+ 'edrv.external.pending': { clientId: string; busy?: boolean }
387
+ 'edrv.external.ack': { token: string; clientId: string; leaseId: string; success: boolean; error?: string }
386
388
  'edrv.external.pendingState': { token: string; take?: boolean }
387
389
  'vscode.devFormGet': {}
388
390
  'vscode.devFormSet': { enabled: boolean; path?: string }
@@ -535,8 +537,9 @@ export interface RpcOkMap {
535
537
  'edrv.unity.install': { project: UnityProjectEntry }
536
538
  'edrv.externalStat': { kind: 'file' | 'directory' | 'missing' }
537
539
  'edrv.external.handoff': { clients: number; token: string }
538
- 'edrv.external.pending': { open: { paths: string[]; line?: number; column?: number } | null }
539
- 'edrv.external.pendingState': { delivered: boolean }
540
+ 'edrv.external.pending': { open: { paths: string[]; line?: number; column?: number; token: string; leaseId: string } | null }
541
+ 'edrv.external.ack': { accepted: boolean }
542
+ 'edrv.external.pendingState': { delivered: boolean; completed?: boolean; error?: string }
540
543
  'vscode.devFormGet': { devForm: DevFormInfo }
541
544
  'vscode.devFormSet': { devForm: DevFormInfo; restart: boolean }
542
545
  'compat': { report: CompatReport }
package/src/shared/svn.ts CHANGED
@@ -799,18 +799,15 @@ export function ignoreItemsOf(paths: readonly string[]): SvnIgnoreItem[] {
799
799
  return [...byDir.entries()].map(([dir, names]) => ({ dir, names }))
800
800
  }
801
801
 
802
- /** 深度分析投递口地址(host RPC 本地免鉴权;agent 用 shell 直接 POST)。 */
803
- const AGENT_SUBMIT_URL = 'http://127.0.0.1:3080/edrv/rpc'
804
-
805
802
  /**
806
803
  * 会话 agent 深度分析任务文本(混合通道 02-deep-session-prompt 修正版)。
807
804
  * 在**独立新会话**草稿箱填入后由用户发送;agent 自跑只读 `svn status` 取清单
808
805
  * (prompt 定长瘦身,不随变更数膨胀),借 codegraph/读文件/sub agent 深度分析,
809
- * 产出方案经免鉴权 RPC `svn.aiPlanSubmit` 投递回面板收件箱——确认与执行仍全部
806
+ * 产出方案经当前会话的 svn_plan_submit 工具投递回面板收件箱,确认与执行仍全部
810
807
  * 走面板(agent 禁写、只读放行)。
811
- * @author ddj 2026年09月23号
808
+ * @author ddj 2026年09月28号
812
809
  * @param wcRoot 工作副本根(取数指引内嵌)
813
- * @param sessionId 会话 id(投递命令内嵌;host 按它解析工作副本)
810
+ * @param sessionId 分析会话 id(仅作说明,不参与工具授权)
814
811
  * @returns 任务文本(定长 ~1KB:取数指引/分析要求/投递命令/硬约束)
815
812
  */
816
813
  export function buildAgentPrompt(wcRoot: string, sessionId: string): string {
@@ -838,8 +835,9 @@ export function buildAgentPrompt(wcRoot: string, sessionId: string): string {
838
835
  '3. 组名用简短英文、不以 - 开头;',
839
836
  '4. 只使用 `svn status` 实际返回的路径,绝不虚构或改写路径。',
840
837
  '',
841
- '分析完成后,用 shell 执行下面的命令把方案投递回面板(把 <PLAN_JSON> 替换为方案 JSON):',
842
- 'Invoke-RestMethod -Uri ' + AGENT_SUBMIT_URL + ' -Method Post -ContentType \'application/json\' -Body (@{ method = \'svn.aiPlanSubmit\'; args = @{ sessionId = \'' + sessionId + '\'; plan = <PLAN_JSON> } } | ConvertTo-Json -Depth 8)',
838
+ '分析完成后,调用 svn_plan_submit 工具,把方案 JSON 作为 plan 参数投递回面板。',
839
+ '工具只接收 { "plan": <PLAN_JSON> },自动使用当前执行会话;不要传入其他 sessionId。',
840
+ '当前分析会话:' + sessionId + '。工具仅保存待审方案,不执行方案中的文件操作。',
843
841
  '',
844
842
  '投递成功会返回 { ok, accepted, dropped };随后我会在面板弹窗中确认执行,你无需再做任何操作。',
845
843
  ].join('\n')
@@ -0,0 +1,51 @@
1
+ /** Session-bound SVN analysis result delivery. @author ddj 2026年09月28号 */
2
+ import { loadHostModule } from './hostImport.js'
3
+ import type { Ctx } from './store.js'
4
+ import type { RpcHandler, RpcResult } from './shared/rpc.js'
5
+
6
+ interface PlanArgs { plan: unknown }
7
+ interface PlanExec { agent?: { session?: { id?: string } }; signal?: AbortSignal }
8
+ type Submit = RpcHandler<'svn.aiPlanSubmit'>
9
+
10
+ /**
11
+ * Build a result-only tool that derives ownership from the executing session.
12
+ * It cannot revert, commit, ignore, or select another session.
13
+ * @author ddj 2026年09月28号
14
+ * @param submit Existing validated plan inbox handler.
15
+ * @returns Definition options for the running Host's defineTool API.
16
+ */
17
+ export function planToolOptions(submit: Submit) {
18
+ /** @author ddj 2026年09月28号 @param args Model plan. @param exec Owning execution context. @returns Validated inbox result. */
19
+ async function execute(args: PlanArgs, exec: PlanExec): Promise<RpcResult<'svn.aiPlanSubmit'>> {
20
+ const sessionId = exec.agent?.session?.id
21
+ if (!sessionId) return { ok: false, error: 'SVN 方案投递需要所属会话' }
22
+ if (exec.signal?.aborted) return { ok: false, error: 'SVN 方案投递已取消' }
23
+ return submit({ sessionId, plan: args.plan })
24
+ }
25
+ /** @author ddj 2026年09月28号 @param _args Request. @param result Inbox result. @returns Model-visible result. */
26
+ function render(_args: PlanArgs, result: RpcResult<'svn.aiPlanSubmit'>) {
27
+ return [{ type: 'text', text: JSON.stringify(result) }]
28
+ }
29
+ return {
30
+ name: 'svn_plan_submit',
31
+ description: 'Submit an SVN change-analysis plan to the current workspace review inbox. This only records the proposal; the user must confirm changes in VSCodeMode. It does not modify or commit files.',
32
+ parameters: { plan: { type: 'json', required: true, description: 'Analysis proposal with groups, reverts and ignores arrays, using paths from the current SVN working-copy status.' } },
33
+ output: { schema: { type: 'json' }, render },
34
+ isConcurrencySafe: false,
35
+ execute,
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Register the SVN submission tool using the running Host API and lifecycle owner.
41
+ * @author ddj 2026年09月28号
42
+ * @param ctx Host plugin context owning the registration.
43
+ * @param submit Validated SVN inbox handler.
44
+ */
45
+ export async function installPlanTool(ctx: Ctx, submit: Submit): Promise<void> {
46
+ const module = await loadHostModule('@deepseek-ai/dsh-tools') as { defineTool: (options: unknown) => unknown }
47
+ const definition = module.defineTool(planToolOptions(submit))
48
+ /** @author ddj 2026年09月28号 @returns Registration disposer owned by this plugin. */
49
+ function install(): () => void { return ctx.get('tools').register(definition) }
50
+ ctx.effect(install, 'vscode-mode:svn-plan-submit')
51
+ }
@@ -7,10 +7,14 @@
7
7
  * package.json name 不是本包 → 拒绝覆盖。纯路径构造与项目特征判定可单测。
8
8
  * 作者 ddj 2026-09-07
9
9
  */
10
- import { cp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
10
+ import { cp, mkdir, readFile, readdir, rm, writeFile, rename } from 'node:fs/promises'
11
11
  import { basename, dirname, join, sep } from 'node:path'
12
12
  import { fileURLToPath } from 'node:url'
13
13
  import { dshHome, PLUGIN_ID } from './paths.js'
14
+ import { randomUUID } from 'node:crypto'
15
+ import { ensureOpenBridge } from './openBridge.js'
16
+ import { checkOpenPath } from './openInbox.js'
17
+ import type { Ctx } from './store.js'
14
18
  import { UNITY_PACKAGE_NAME } from './shared/integration.js'
15
19
  import type { UnityListPayload, UnityProjectEntry } from './shared/integration.js'
16
20
 
@@ -21,6 +25,8 @@ const PROJECTS_FILE = 'unity-projects.json'
21
25
  /** 包源目录相对插件包根的位置。 */
22
26
  const UNITY_DIR_NAME = 'unity'
23
27
 
28
+ export interface UnityBridgeOpts { ctx: Ctx; baseUrl: string }
29
+
24
30
  /**
25
31
  * 包源目录(随插件包分发的 unity/com.dsh.editor 绝对路径)。
26
32
  * @author ddj 2026年09月07号
@@ -192,15 +198,17 @@ export async function unityRemove(rawPath: string, home = dshHome()): Promise<vo
192
198
  * @param moduleUrl 模块 URL(测试注入)
193
199
  * @returns 安装后项目状态
194
200
  */
195
- export async function unityInstall(rawPath: string, moduleUrl: string = import.meta.url): Promise<UnityProjectEntry> {
201
+ export async function unityInstall(rawPath: string, moduleUrl: string = import.meta.url, bridge?: UnityBridgeOpts): Promise<UnityProjectEntry> {
196
202
  const root = normalizeUnityRoot(rawPath)
197
203
  const source = unitySourceDir(moduleUrl)
198
204
  const target = unityTargetOf(root)
199
205
  await validateInstallTarget(root, source, target)
206
+ const bridgeConfigPath = bridge ? await ensureOpenBridge(bridge.ctx, bridge.baseUrl) : undefined
200
207
  await rm(target, { recursive: true, force: true })
201
208
  await mkdir(join(target, '..'), { recursive: true })
202
209
  await cp(source, target, { recursive: true })
203
- return unityEntryOf(root, moduleUrl)
210
+ if (bridgeConfigPath) await writeUnityHint(root, bridgeConfigPath)
211
+ return { ...(await unityEntryOf(root, moduleUrl)), bridgeConfigPath }
204
212
  }
205
213
 
206
214
  /**
@@ -232,3 +240,18 @@ async function validateInstallTarget(root: string, source: string, target: strin
232
240
  } catch { /* 解析失败按未知包处理 */ }
233
241
  if (name !== UNITY_PACKAGE_NAME) throw new Error('目标目录已存在且不是 DSH 包,拒绝覆盖:' + target)
234
242
  }
243
+
244
+ /** @private @author ddj 2026年09月28号
245
+ * Bind a project's machine-local configuration to its selected profile bridge.
246
+ * @param root Validated Unity project root.
247
+ * @param configPath Absolute profile bridge INI path.
248
+ */
249
+ async function writeUnityHint(root: string, configPath: string): Promise<void> {
250
+ const dir = join(root, 'UserSettings')
251
+ await mkdir(dir, { recursive: true })
252
+ await checkOpenPath(dir)
253
+ const target = join(dir, 'dsh-editor.ini')
254
+ const temporary = target + '.' + randomUUID() + '.tmp'
255
+ await writeFile(temporary, '[dsh]\nconfig=' + configPath + '\n', { flag: 'wx', mode: 0o600 })
256
+ try { await rename(temporary, target) } finally { await rm(temporary, { force: true }) }
257
+ }