dsh-vscode-mode 0.11.0 → 0.13.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.
Files changed (76) hide show
  1. package/README.md +158 -111
  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 +13362 -11806
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +4110 -2507
  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/activePathStore.ts +28 -0
  14. package/src/client/addToConversation.ts +4 -2
  15. package/src/client/ai/inlineProvider.ts +1 -8
  16. package/src/client/commandBridge.ts +14 -42
  17. package/src/client/externalOpen.ts +40 -19
  18. package/src/client/index.ts +101 -18
  19. package/src/client/keybindings.ts +36 -73
  20. package/src/client/monaco/loader.ts +3 -1
  21. package/src/client/openFlow.ts +36 -28
  22. package/src/client/openReceipt.ts +64 -0
  23. package/src/client/panelCommands.ts +101 -0
  24. package/src/client/pdf/pdfLoader.ts +4 -2
  25. package/src/client/receiptState.ts +120 -0
  26. package/src/client/rpc.ts +30 -4
  27. package/src/client/shortcutRecorder.ts +161 -0
  28. package/src/client/shortcutsOfficial.ts +460 -0
  29. package/src/client/sidebar/panels/RulesPanel.ts +15 -11
  30. package/src/client/sidebarBridge.ts +7 -0
  31. package/src/client/ui/DiffBadge.ts +2 -1
  32. package/src/client/ui/EditorView.ts +246 -137
  33. package/src/client/ui/McpSettings.ts +10 -8
  34. package/src/client/ui/OfficialSideTab.ts +5 -2
  35. package/src/client/ui/QuickOpen.ts +16 -16
  36. package/src/client/ui/ShortcutSettings.ts +274 -0
  37. package/src/client/ui/commandCatalog.ts +39 -43
  38. package/src/client/useOpenReceipt.ts +128 -0
  39. package/src/compat.ts +5 -3
  40. package/src/dap/manager.ts +46 -13
  41. package/src/devForm.ts +252 -92
  42. package/src/externalHandoff.ts +93 -52
  43. package/src/fileOpenSettings.ts +4 -24
  44. package/src/hostImport.ts +19 -0
  45. package/src/httpGuard.ts +101 -0
  46. package/src/index.ts +106 -97
  47. package/src/integrate.ts +56 -50
  48. package/src/lsp/transport.ts +84 -127
  49. package/src/mcp.ts +58 -9
  50. package/src/mcpIsolation.ts +211 -102
  51. package/src/mcpProject.ts +220 -133
  52. package/src/mcpRuntime.ts +343 -0
  53. package/src/openBridge.ts +69 -0
  54. package/src/openInbox.ts +207 -0
  55. package/src/paths.ts +8 -5
  56. package/src/processTree.ts +63 -0
  57. package/src/reveal.ts +5 -3
  58. package/src/routes.ts +156 -154
  59. package/src/rpc.ts +14 -13
  60. package/src/rules.ts +100 -44
  61. package/src/shared/appUrl.ts +19 -0
  62. package/src/shared/externalOpen.ts +71 -0
  63. package/src/shared/integration.ts +2 -0
  64. package/src/shared/keybindings.ts +256 -0
  65. package/src/shared/mcp.ts +4 -1
  66. package/src/shared/rpc.ts +7 -4
  67. package/src/shared/rules.ts +9 -1
  68. package/src/shared/svn.ts +6 -8
  69. package/src/svnPlanTool.ts +51 -0
  70. package/src/unityBridge.ts +26 -3
  71. package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
  72. package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
  73. package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
  74. package/unity/com.dsh.editor/README.md +16 -25
  75. package/unity/com.dsh.editor/package.json +2 -2
  76. package/src/client/ui/KeybindingsPanel.ts +0 -190
@@ -5,6 +5,8 @@
5
5
  * `|` 连接多个候选(任一命中即触发),如 `Alt+ArrowLeft|Ctrl+Alt+-`。
6
6
  * 约束:本模块被 host 与 client 两半共用,**禁止 import client 侧模块**(React/浏览器 API)。
7
7
  * 与 client/ui/commandCatalog 的一致性由 tests/commands.test.ts 断言兜底。
8
+ * 官方机制(2026-10):SHORTCUT_PROFILES 描述各命令在官方 ctx.shortcuts 的 profile 默认键位
9
+ * (由 client 注册官方服务时使用);chordToBinding/bindingToChord 负责插件弦与官方绑定互转。
8
10
  * 作者 ddj 2026年08月26号 / 2026年09月10号
9
11
  */
10
12
 
@@ -66,3 +68,257 @@ export function normalizeKeybindings(raw: unknown): Record<string, string> {
66
68
  }
67
69
  return out
68
70
  }
71
+
72
+ //#region 官方 shortcuts 机制映射层(纯数据/纯函数,禁 import client 侧与官方包)
73
+
74
+ /** 官方 shortcuts 服务的修饰符标记('primary' = 桌面端按平台展开为 Ctrl/Cmd,web 同语义)。 */
75
+ export type OfficialModifier = 'primary' | 'control' | 'alt' | 'shift' | 'meta'
76
+
77
+ /** 官方绑定形状(物理 code + 修饰符;与 @deepseek-ai/dsh-client-shortcuts 绑定契约同构,鸭子类型)。 */
78
+ export interface OfficialBinding {
79
+ code: string
80
+ modifiers: OfficialModifier[]
81
+ secondCode?: string
82
+ }
83
+
84
+ /** 官方 profile 键(desktop|web × macos|windows|linux)。 */
85
+ export type ShortcutProfileKey =
86
+ | 'desktop:macos' | 'desktop:windows' | 'desktop:linux'
87
+ | 'web:macos' | 'web:windows' | 'web:linux'
88
+
89
+ /** 命令 id → 各 profile 官方默认键位(缺 profile = 该端不绑定)。 */
90
+ export type ShortcutProfiles = Partial<Record<ShortcutProfileKey, OfficialBinding>>
91
+
92
+ /** 显示主键 → 官方物理 code 对照(官方 code 白名单:Key[A-Z]|Digit[0-9]|F1-24|具名键)。 */
93
+ const DISPLAY_KEY_TO_CODE: Record<string, string> = {
94
+ '-': 'Minus', '=': 'Equal', '[': 'BracketLeft', ']': 'BracketRight',
95
+ ';': 'Semicolon', "'": 'Quote', ',': 'Comma', '.': 'Period',
96
+ '/': 'Slash', '\\': 'Backslash', '`': 'Backquote',
97
+ Enter: 'Enter', Tab: 'Tab', Backspace: 'Backspace', Delete: 'Delete',
98
+ Escape: 'Escape', Space: 'Space',
99
+ ArrowUp: 'ArrowUp', ArrowDown: 'ArrowDown', ArrowLeft: 'ArrowLeft', ArrowRight: 'ArrowRight',
100
+ }
101
+
102
+ /** 官方具名 code(非 Key/Digit/F 前缀且可回显的部分)。 */
103
+ const NAMED_CODES = ['Enter', 'Tab', 'Backspace', 'Delete', 'Escape', 'Space',
104
+ 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'] as const
105
+
106
+ /** 官方 code → 显示主键(符号键反向对照)。 */
107
+ const CODE_TO_DISPLAY_KEY: Record<string, string> = {
108
+ Minus: '-', Equal: '=', BracketLeft: '[', BracketRight: ']',
109
+ Semicolon: ';', Quote: "'", Comma: ',', Period: '.',
110
+ Slash: '/', Backslash: '\\', Backquote: '`',
111
+ }
112
+
113
+ /** 显示主键 → 官方 code(PageUp/PageDown/Home/End/Insert 等官方不支持 → null)。 */
114
+ function keyToCode(key: string): string | null {
115
+ if (Object.hasOwn(DISPLAY_KEY_TO_CODE, key)) return DISPLAY_KEY_TO_CODE[key]
116
+ if (key.length === 1 && key >= 'a' && key <= 'z') return 'Key' + key.toUpperCase()
117
+ if (key.length === 1 && key >= 'A' && key <= 'Z') return 'Key' + key
118
+ if (key.length === 1 && key >= '0' && key <= '9') return 'Digit' + key
119
+ if (/^F([1-9]|1[0-9]|2[0-4])$/u.test(key)) return key
120
+ return null
121
+ }
122
+
123
+ /** 官方 code → 显示主键(不可回显 → null)。 */
124
+ function codeToKey(code: string): string | null {
125
+ if (Object.hasOwn(CODE_TO_DISPLAY_KEY, code)) return CODE_TO_DISPLAY_KEY[code]
126
+ if (/^Key[A-Z]$/u.test(code)) return code.slice(3)
127
+ if (/^Digit[0-9]$/u.test(code)) return code.slice(5)
128
+ if (/^F([1-9]|1[0-9]|2[0-4])$/u.test(code)) return code
129
+ if ((NAMED_CODES as readonly string[]).includes(code)) return code
130
+ return null
131
+ }
132
+
133
+ /** 插件弦的修饰符 token → 官方修饰符(Ctrl 与 Cmd 同为 primary,与 matchEvent 的互认语义一致)。 */
134
+ const CHORD_MODIFIER_TOKENS: Record<string, OfficialModifier> = {
135
+ ctrl: 'primary', control: 'primary', cmd: 'primary', meta: 'primary',
136
+ shift: 'shift', alt: 'alt',
137
+ }
138
+
139
+ /**
140
+ * 插件键位弦(单候选)→ 官方绑定。
141
+ * 不可表示的主键(PageUp/PageDown/Home/End/Insert 等)返回 null,调用方跳过。
142
+ * @author ddj 2026年10月
143
+ * @param chord 键位弦(如 `Ctrl+Alt+-` / `F9` / `Alt+ArrowLeft`)
144
+ * @returns 官方绑定或 null
145
+ */
146
+ export function chordToBinding(chord: string): OfficialBinding | null {
147
+ const parts = String(chord ?? '').split('+').map((part) => part.trim()).filter(Boolean)
148
+ if (!parts.length) return null
149
+ const modifiers = new Set<OfficialModifier>()
150
+ let key = ''
151
+ for (const part of parts) {
152
+ const modifier = CHORD_MODIFIER_TOKENS[part.toLowerCase()]
153
+ if (modifier) {
154
+ modifiers.add(modifier)
155
+ continue
156
+ }
157
+ if (key) return null
158
+ key = part
159
+ }
160
+ if (!key) return null
161
+ if (key === ' ') key = 'Space'
162
+ else if (key.length === 1 && key >= 'a' && key <= 'z') key = key.toUpperCase()
163
+ const code = keyToCode(key)
164
+ return code === null ? null : { code, modifiers: [...modifiers] }
165
+ }
166
+
167
+ /**
168
+ * 官方绑定 → 可读键位弦,保留规范化后的 Control/Meta 物理区别。
169
+ * 官方目录展示优先使用官方 keys;此函数仅作为缺少展示字段时的回落。
170
+ * @author ddj 2026年09月28号
171
+ * @param binding 官方绑定
172
+ * @returns 插件弦(不可表示 → null)
173
+ */
174
+ export function bindingToChord(binding: OfficialBinding): string | null {
175
+ if (!binding || typeof binding !== 'object') return null
176
+ const key = codeToKey(binding.code)
177
+ if (key === null) return null
178
+ let suffix = ''
179
+ if (binding.secondCode !== undefined) {
180
+ const secondKey = codeToKey(binding.secondCode)
181
+ if (secondKey === null) return null
182
+ suffix = '+' + secondKey
183
+ }
184
+ const parts: string[] = []
185
+ if (binding.modifiers.includes('primary') || binding.modifiers.includes('control')) parts.push('Ctrl')
186
+ if (binding.modifiers.includes('meta')) parts.push('Meta')
187
+ if (binding.modifiers.includes('shift')) parts.push('Shift')
188
+ if (binding.modifiers.includes('alt')) parts.push('Alt')
189
+ return [...parts, key + suffix].join('+')
190
+ }
191
+
192
+ /** 官方绑定字面量构造(表格可读性辅助)。 */
193
+ function bind(code: string, modifiers: OfficialModifier[]): OfficialBinding {
194
+ return { code, modifiers }
195
+ }
196
+
197
+ /**
198
+ * 命令 id → 官方 profile 默认键位(与 KEYBINDING_DEFAULTS 一一对应;client 注册官方服务的数据源)。
199
+ * 取值约束(对齐官方 bindingIssue/isWebBindingAllowed 校验,错报会在注册时抛错):
200
+ * - desktop:macos/windows:官方不做保留校验,用 VS Code 同款 chord;
201
+ * - desktop:linux / web:linux:Arrow*、裸 F 键、全 Shift、Ctrl+V/C/X/Z/Y/Q/H/A、Meta 均为保留键 → 不声明;
202
+ * - web:macos/windows:单修饰符组合不在白名单 → 用 primary+alt / primary+shift 变体(官方 ui-layout 同款);
203
+ * - Ctrl+PageDown/PageUp 候选废弃(官方 code 白名单无 PageUp/PageDown);
204
+ * - edrv.toggleSidebar 留空:Ctrl+B 与官方 sidebar.left.toggle(Mod+B)冲突,注册会抛错且冲突键互相阻断。
205
+ * @author ddj 2026年10月
206
+ */
207
+ export const SHORTCUT_PROFILES: Record<string, ShortcutProfiles> = {
208
+ 'edrv.toggleSidebar': {},
209
+ 'edrv.save': {
210
+ 'desktop:macos': bind('KeyS', ['primary']),
211
+ 'desktop:windows': bind('KeyS', ['primary']),
212
+ 'desktop:linux': bind('KeyS', ['primary']),
213
+ },
214
+ // 官方 workspace.files 已占用桌面 primary+P 和 Web primary+alt+P;插件 QuickOpen 使用非冲突组合。
215
+ 'edrv.quickOpen': {
216
+ 'desktop:macos': bind('KeyP', ['primary', 'alt']),
217
+ 'desktop:windows': bind('KeyP', ['primary', 'alt']),
218
+ 'desktop:linux': bind('KeyP', ['primary', 'alt']),
219
+ 'web:macos': bind('KeyP', ['primary', 'alt', 'shift']),
220
+ 'web:windows': bind('KeyP', ['primary', 'alt', 'shift']),
221
+ },
222
+ // 官方 session.fork 占用两种 Web primary+shift+F;加 Alt 避开联合目录冲突。
223
+ 'edrv.searchInFiles': {
224
+ 'desktop:macos': bind('KeyF', ['primary', 'shift']),
225
+ 'desktop:windows': bind('KeyF', ['primary', 'shift']),
226
+ 'desktop:linux': bind('KeyF', ['primary', 'shift']),
227
+ 'web:macos': bind('KeyF', ['primary', 'alt', 'shift']),
228
+ 'web:windows': bind('KeyF', ['primary', 'alt', 'shift']),
229
+ },
230
+ // Ctrl+Shift+V(primary+KeyV)在 linux 为保留键(官方 bindingIssue 不豁免 shift),desktop:linux 不声明
231
+ 'edrv.toggleMarkdownPreview': {
232
+ 'desktop:macos': bind('KeyV', ['primary', 'shift']),
233
+ 'desktop:windows': bind('KeyV', ['primary', 'shift']),
234
+ 'web:macos': bind('KeyV', ['primary', 'shift']),
235
+ 'web:windows': bind('KeyV', ['primary', 'shift']),
236
+ },
237
+ // 导航历史:主候选 Alt+Arrow 在 linux/web 为保留或白名单外;web 取第二候选(Ctrl+Alt+- / Ctrl+Shift+-)
238
+ 'edrv.navigateBack': {
239
+ 'desktop:macos': bind('ArrowLeft', ['alt']),
240
+ 'desktop:windows': bind('ArrowLeft', ['alt']),
241
+ 'web:macos': bind('Minus', ['primary', 'alt']),
242
+ 'web:windows': bind('Minus', ['primary', 'alt']),
243
+ },
244
+ 'edrv.navigateForward': {
245
+ 'desktop:macos': bind('ArrowRight', ['alt']),
246
+ 'desktop:windows': bind('ArrowRight', ['alt']),
247
+ 'web:macos': bind('Minus', ['primary', 'shift']),
248
+ 'web:windows': bind('Minus', ['primary', 'shift']),
249
+ },
250
+ // 页签循环:Ctrl+PageDown/PageUp 候选官方不可表示,仅保留 Ctrl+Alt+Arrow 主候选
251
+ 'edrv.nextTab': {
252
+ 'desktop:macos': bind('ArrowRight', ['primary', 'alt']),
253
+ 'desktop:windows': bind('ArrowRight', ['primary', 'alt']),
254
+ 'web:macos': bind('ArrowRight', ['primary', 'alt']),
255
+ 'web:windows': bind('ArrowRight', ['primary', 'alt']),
256
+ },
257
+ 'edrv.prevTab': {
258
+ 'desktop:macos': bind('ArrowLeft', ['primary', 'alt']),
259
+ 'desktop:windows': bind('ArrowLeft', ['primary', 'alt']),
260
+ 'web:macos': bind('ArrowLeft', ['primary', 'alt']),
261
+ 'web:windows': bind('ArrowLeft', ['primary', 'alt']),
262
+ },
263
+ 'edrv.goToLine': {
264
+ 'desktop:macos': bind('KeyG', ['primary']),
265
+ 'desktop:windows': bind('KeyG', ['primary']),
266
+ 'desktop:linux': bind('KeyG', ['primary']),
267
+ },
268
+ 'edrv.showCommands': {
269
+ 'desktop:macos': bind('KeyP', ['primary', 'shift']),
270
+ 'desktop:windows': bind('KeyP', ['primary', 'shift']),
271
+ 'desktop:linux': bind('KeyP', ['primary', 'shift']),
272
+ 'web:macos': bind('KeyP', ['primary', 'shift']),
273
+ 'web:windows': bind('KeyP', ['primary', 'shift']),
274
+ },
275
+ 'edrv.nextEditorRow': {
276
+ 'desktop:macos': bind('ArrowDown', ['primary', 'alt']),
277
+ 'desktop:windows': bind('ArrowDown', ['primary', 'alt']),
278
+ 'web:macos': bind('ArrowDown', ['primary', 'alt']),
279
+ 'web:windows': bind('ArrowDown', ['primary', 'alt']),
280
+ },
281
+ 'edrv.prevEditorRow': {
282
+ 'desktop:macos': bind('ArrowUp', ['primary', 'alt']),
283
+ 'desktop:windows': bind('ArrowUp', ['primary', 'alt']),
284
+ 'web:macos': bind('ArrowUp', ['primary', 'alt']),
285
+ 'web:windows': bind('ArrowUp', ['primary', 'alt']),
286
+ },
287
+ 'edrv.addSelectionRef': {
288
+ 'desktop:macos': bind('KeyU', ['primary']),
289
+ 'desktop:windows': bind('KeyU', ['primary']),
290
+ 'desktop:linux': bind('KeyU', ['primary']),
291
+ },
292
+ 'edrv.closeTab': {
293
+ 'desktop:macos': bind('F4', ['primary']),
294
+ 'desktop:windows': bind('F4', ['primary']),
295
+ 'desktop:linux': bind('F4', ['primary']),
296
+ },
297
+ // 调试 F 键:linux 判 modifier-required(裸 F/全 Shift),仅 desktop:macos/windows
298
+ 'edrv.debugToggleBreakpoint': {
299
+ 'desktop:macos': bind('F9', []),
300
+ 'desktop:windows': bind('F9', []),
301
+ },
302
+ 'edrv.debugStartContinue': {
303
+ 'desktop:macos': bind('F5', []),
304
+ 'desktop:windows': bind('F5', []),
305
+ },
306
+ 'edrv.debugStepOver': {
307
+ 'desktop:macos': bind('F10', []),
308
+ 'desktop:windows': bind('F10', []),
309
+ },
310
+ 'edrv.debugStepInto': {
311
+ 'desktop:macos': bind('F11', []),
312
+ 'desktop:windows': bind('F11', []),
313
+ },
314
+ 'edrv.debugStepOut': {
315
+ 'desktop:macos': bind('F11', ['shift']),
316
+ 'desktop:windows': bind('F11', ['shift']),
317
+ },
318
+ 'edrv.debugStop': {
319
+ 'desktop:macos': bind('F5', ['shift']),
320
+ 'desktop:windows': bind('F5', ['shift']),
321
+ },
322
+ }
323
+
324
+ //#endregion
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 }
@@ -4,7 +4,10 @@
4
4
  * 作者 ddj 2026年09月03号
5
5
  */
6
6
 
7
- /** 规则作用域:用户规则(~/.dsh/rules)或项目规则(<工作区>/.dsh/rules)。 */
7
+ /** 用户规则来源:当前 DSH_HOME 或系统用户目录的旧版 .dsh。 */
8
+ export type UserRuleOrigin = 'current' | 'legacy'
9
+
10
+ /** 规则作用域:用户规则(DSH_HOME/rules + ~/.dsh/rules)或项目规则(<工作区>/.dsh/rules)。 */
8
11
  export type RuleScope = 'user' | 'project'
9
12
 
10
13
  /** 规则生效类型(由 frontmatter 推导):总是 / 自动(globs 命中时)/ 手动(仅索引)。 */
@@ -13,6 +16,8 @@ export type RuleType = 'always' | 'auto' | 'manual'
13
16
  /** 一条 .mdc 规则的元信息(列表行展示 + 注入语义所需的最小集)。 */
14
17
  export interface RuleInfo {
15
18
  scope: RuleScope
19
+ /** 用户规则的实际来源;项目规则不设置。 */
20
+ origin?: UserRuleOrigin
16
21
  /** 文件名(含 .mdc 后缀,不含路径)。 */
17
22
  file: string
18
23
  /** 绝对路径(host 解析,供展示与按需读取)。 */
@@ -48,6 +53,8 @@ export interface RuleSaveInput {
48
53
  scope: RuleScope
49
54
  /** project 必填:目标工作区绝对路径(须为 DSH 已注册 workspace)。 */
50
55
  workspacePath?: string
56
+ /** 缺省 current;仅用户规则可指定 legacy。 */
57
+ origin?: UserRuleOrigin
51
58
  file: string
52
59
  content: string
53
60
  }
@@ -56,5 +63,6 @@ export interface RuleSaveInput {
56
63
  export interface RuleRefInput {
57
64
  scope: RuleScope
58
65
  workspacePath?: string
66
+ origin?: UserRuleOrigin
59
67
  file: string
60
68
  }
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
+ }