dsh-vscode-mode 0.12.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.
- package/README.md +129 -103
- package/assets/shell/dsh-open.cs +65 -149
- package/assets/shell/dsh-open.mjs +241 -0
- package/assets/shell/dsh-open.ps1 +28 -47
- package/assets/shell/dsh-open.sh +25 -66
- package/lib/client.js +12465 -11526
- package/lib/client.js.map +1 -1
- package/lib/index.js +3889 -2336
- package/lib/index.js.map +1 -1
- package/package.json +9 -8
- package/skills/dsh-vscodemode-mcp/SKILL.md +94 -132
- package/src/childEnv.ts +19 -0
- package/src/client/addToConversation.ts +4 -2
- package/src/client/ai/inlineProvider.ts +1 -8
- package/src/client/commandBridge.ts +6 -2
- package/src/client/externalOpen.ts +40 -19
- package/src/client/index.ts +10 -4
- package/src/client/keybindings.ts +18 -1
- package/src/client/monaco/loader.ts +3 -1
- package/src/client/openFlow.ts +36 -28
- package/src/client/openReceipt.ts +64 -0
- package/src/client/panelCommands.ts +101 -0
- package/src/client/pdf/pdfLoader.ts +4 -2
- package/src/client/receiptState.ts +120 -0
- package/src/client/rpc.ts +30 -4
- package/src/client/shortcutRecorder.ts +161 -0
- package/src/client/shortcutsOfficial.ts +13 -4
- package/src/client/sidebarBridge.ts +7 -0
- package/src/client/ui/DiffBadge.ts +2 -1
- package/src/client/ui/EditorView.ts +237 -55
- package/src/client/ui/McpSettings.ts +6 -3
- package/src/client/ui/OfficialSideTab.ts +5 -2
- package/src/client/ui/QuickOpen.ts +10 -9
- package/src/client/ui/ShortcutSettings.ts +34 -54
- package/src/client/ui/commandCatalog.ts +5 -4
- package/src/client/useOpenReceipt.ts +128 -0
- package/src/compat.ts +5 -3
- package/src/dap/manager.ts +46 -13
- package/src/devForm.ts +252 -92
- package/src/externalHandoff.ts +93 -52
- package/src/fileOpenSettings.ts +1 -24
- package/src/hostImport.ts +19 -0
- package/src/httpGuard.ts +101 -0
- package/src/index.ts +106 -97
- package/src/integrate.ts +56 -50
- package/src/lsp/transport.ts +84 -127
- package/src/mcp.ts +58 -9
- package/src/mcpIsolation.ts +211 -102
- package/src/mcpProject.ts +220 -133
- package/src/mcpRuntime.ts +343 -0
- package/src/openBridge.ts +69 -0
- package/src/openInbox.ts +207 -0
- package/src/paths.ts +8 -5
- package/src/processTree.ts +63 -0
- package/src/reveal.ts +5 -3
- package/src/routes.ts +156 -154
- package/src/rpc.ts +14 -13
- package/src/shared/appUrl.ts +19 -0
- package/src/shared/externalOpen.ts +71 -0
- package/src/shared/integration.ts +2 -0
- package/src/shared/keybindings.ts +8 -5
- package/src/shared/mcp.ts +4 -1
- package/src/shared/rpc.ts +7 -4
- package/src/shared/svn.ts +6 -8
- package/src/svnPlanTool.ts +51 -0
- package/src/unityBridge.ts +26 -3
- package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
- package/unity/com.dsh.editor/README.md +16 -25
- package/unity/com.dsh.editor/package.json +2 -2
|
@@ -11,22 +11,12 @@
|
|
|
11
11
|
import React from 'react'
|
|
12
12
|
import { COMMANDS } from '../keybindings.js'
|
|
13
13
|
import { log } from '../log.js'
|
|
14
|
+
import { startRecording } from '../shortcutRecorder.js'
|
|
14
15
|
import { resetPluginKeys } from '../shortcutsOfficial.js'
|
|
15
16
|
import type { ShortcutsServiceLike, ShortcutCatalogRow } from '../shortcutsOfficial.js'
|
|
16
17
|
|
|
17
18
|
const settingsLog = log.child('shortcut-settings')
|
|
18
19
|
|
|
19
|
-
const MODIFIER_ONLY = new Set(['Control', 'Shift', 'Alt', 'Meta', 'AltGraph'])
|
|
20
|
-
|
|
21
|
-
/** 录制键 → 官方绑定(物理 code + 修饰符;Ctrl/Cmd 同为 primary,与官方互认语义一致)。 */
|
|
22
|
-
function bindingFromEvent(e) {
|
|
23
|
-
const modifiers = []
|
|
24
|
-
if (e.ctrlKey || e.metaKey) modifiers.push('primary')
|
|
25
|
-
if (e.altKey) modifiers.push('alt')
|
|
26
|
-
if (e.shiftKey) modifiers.push('shift')
|
|
27
|
-
return { code: e.code, modifiers }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
20
|
/** 目录行展示键位(keys 已按平台格式化;binding null = 未绑定)。 */
|
|
31
21
|
function keysText(row) {
|
|
32
22
|
if (!row || row.binding === null) return '未绑定'
|
|
@@ -114,7 +104,7 @@ function OfficialRow({ row }) {
|
|
|
114
104
|
row.modified && React.createElement('small', { className: 'vsm-kb-warn' }, '已自定义'),
|
|
115
105
|
row.issue && React.createElement('small', { className: 'vsm-kb-warn' }, '键位不受当前环境支持'),
|
|
116
106
|
row.conflicts.length > 0 && React.createElement('small', { className: 'vsm-kb-warn' }, '与 ' + row.conflicts.join('、') + ' 冲突')),
|
|
117
|
-
React.createElement('kbd', { className: 'vsm-kb-key' + (row.conflicts.length ? ' warn' : '') }, keysText(row)),
|
|
107
|
+
React.createElement('kbd', { 'aria-label': row?.aria, className: 'vsm-kb-key' + (row.conflicts.length ? ' warn' : '') }, keysText(row)),
|
|
118
108
|
)
|
|
119
109
|
}
|
|
120
110
|
|
|
@@ -122,11 +112,13 @@ function OfficialRow({ row }) {
|
|
|
122
112
|
function OfficialShortcutsPane({ service }) {
|
|
123
113
|
const [error, setError] = React.useState('')
|
|
124
114
|
const rows = service.catalog.getSnapshot()
|
|
115
|
+
const shortcut = rows.find((row) => row.id === 'shortcuts.open')
|
|
116
|
+
const hint = shortcut?.keys?.length ? '(' + shortcut.keys.join(' ') + ')' : ''
|
|
125
117
|
const config = service.config.getSnapshot()
|
|
126
118
|
const openOfficial = () => {
|
|
127
119
|
setError('')
|
|
128
120
|
if (!service.registry || typeof service.registry.invoke !== 'function') {
|
|
129
|
-
setError('
|
|
121
|
+
setError('官方弹窗入口不可用;可到 设置 → 通用 → 快捷键 打开')
|
|
130
122
|
return
|
|
131
123
|
}
|
|
132
124
|
try {
|
|
@@ -137,15 +129,15 @@ function OfficialShortcutsPane({ service }) {
|
|
|
137
129
|
})
|
|
138
130
|
} catch (e) {
|
|
139
131
|
settingsLog.warn('打开官方快捷键弹窗失败:' + String(e))
|
|
140
|
-
setError('打开官方弹窗失败:' + String(e) + '
|
|
132
|
+
setError('打开官方弹窗失败:' + String(e) + ';可到 设置 → 通用 → 快捷键 打开')
|
|
141
133
|
}
|
|
142
134
|
}
|
|
143
135
|
return React.createElement(React.Fragment, null,
|
|
144
136
|
React.createElement('section', { className: 'vsm-panel' },
|
|
145
137
|
React.createElement('h3', { className: 'vsm-panel-title' }, '官方快捷键配置'),
|
|
146
138
|
React.createElement('div', { className: 'vsm-panel-body' },
|
|
147
|
-
React.createElement('p', null, '
|
|
148
|
-
React.createElement('button', { className: 'vsm-primary vsm-small', onClick: openOfficial }, '
|
|
139
|
+
React.createElement('p', null, '官方快捷键弹窗提供全部命令的查看、录键与恢复默认;也可从 设置 → 通用 → 快捷键 打开。下方为当前目录只读总览(含插件注册的命令)。'),
|
|
140
|
+
React.createElement('button', { className: 'vsm-primary vsm-small', onClick: openOfficial, 'aria-keyshortcuts': shortcut?.aria }, '打开官方快捷键设置' + hint),
|
|
149
141
|
error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
|
|
150
142
|
config?.status === 'unreadable' && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' },
|
|
151
143
|
'快捷键配置读取失败(内容损坏或由更新版本写入),当前使用默认键位;请备份后清理配置,或升级 DSH。')),
|
|
@@ -161,6 +153,7 @@ function OfficialShortcutsPane({ service }) {
|
|
|
161
153
|
|
|
162
154
|
/** 子页二:插件命令键位(行内录键 + 行级清除 + 恢复默认;写入走官方 edit API)。 */
|
|
163
155
|
function PluginShortcutEditor({ service }) {
|
|
156
|
+
const [nativeReady, setNativeReady] = React.useState(false)
|
|
164
157
|
const [recording, setRecording] = React.useState(null) // 正在录制的命令 id
|
|
165
158
|
const [busy, setBusy] = React.useState(false)
|
|
166
159
|
const [message, setMessage] = React.useState('')
|
|
@@ -225,46 +218,33 @@ function PluginShortcutEditor({ service }) {
|
|
|
225
218
|
}
|
|
226
219
|
}
|
|
227
220
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Own capture and native suspension for the selected command's entire lifetime.
|
|
223
|
+
* @author ddj 2026年09月28号
|
|
224
|
+
* @returns Cleanup on cancel, save, failure, service change and unmount.
|
|
225
|
+
*/
|
|
226
|
+
const recordEffect = () => {
|
|
231
227
|
if (!recording) return undefined
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const id = recording
|
|
247
|
-
setRecording(null)
|
|
248
|
-
void saveBinding(id, bindingFromEvent(e))
|
|
249
|
-
}
|
|
250
|
-
window.addEventListener('keydown', onKey, true)
|
|
251
|
-
return () => window.removeEventListener('keydown', onKey, true)
|
|
252
|
-
}, [recording])
|
|
253
|
-
|
|
254
|
-
// 录制期间通知 Desktop 原生桥暂停拦截(Web 适配器为空实现)
|
|
255
|
-
React.useEffect(() => {
|
|
256
|
-
if (!service || typeof service.recording !== 'function') return undefined
|
|
257
|
-
void service.recording(Boolean(recording)).catch(() => {})
|
|
258
|
-
return () => {
|
|
259
|
-
if (recording) void service.recording(false).catch(() => {})
|
|
260
|
-
}
|
|
261
|
-
}, [recording, service])
|
|
228
|
+
let mounted = true
|
|
229
|
+
setNativeReady(false)
|
|
230
|
+
/** @author ddj 2026年09月28号 Mark the native bridge ready to collect physical keys. */
|
|
231
|
+
const ready = () => { if (mounted) setNativeReady(true) }
|
|
232
|
+
/** @author ddj 2026年09月28号 Cancel capture without modifying preferences. */
|
|
233
|
+
const cancel = () => { if (mounted) setRecording(null) }
|
|
234
|
+
/** @author ddj 2026年09月28号 @param binding Accepted physical binding or reset. */
|
|
235
|
+
const save = (binding) => { if (mounted) { setRecording(null); void saveBinding(recording, binding) } }
|
|
236
|
+
/** @author ddj 2026年09月28号 @param reason Recorder failure to show while mounted. */
|
|
237
|
+
const fail = (reason) => { if (mounted) { setRecording(null); setError('录制失败:' + String(reason)) } }
|
|
238
|
+
const dispose = startRecording(service, window, { ready, cancel, save, error: fail })
|
|
239
|
+
return () => { mounted = false; dispose() }
|
|
240
|
+
}
|
|
241
|
+
React.useEffect(recordEffect, [recording, service])
|
|
262
242
|
|
|
263
243
|
const rowsView = rows.map(({ id, label, row }) => {
|
|
264
244
|
const recordingRow = recording === id
|
|
265
245
|
const display = recordingRow
|
|
266
|
-
? React.createElement('span', { className: 'vsm-kb-rec' }, '按下组合键…(Esc 取消 / Backspace
|
|
267
|
-
: React.createElement('kbd', { className: 'vsm-kb-key' + (row && row.conflicts.length ? ' warn' : '') }, keysText(row))
|
|
246
|
+
? React.createElement('span', { className: 'vsm-kb-rec' }, nativeReady ? '按下组合键…(Esc 取消 / Backspace 恢复默认)' : '正在准备录制…')
|
|
247
|
+
: React.createElement('kbd', { 'aria-label': row?.aria, className: 'vsm-kb-key' + (row && row.conflicts.length ? ' warn' : '') }, keysText(row))
|
|
268
248
|
return React.createElement('div', { key: id, className: 'vsm-kb-row' + (recordingRow ? ' rec' : '') },
|
|
269
249
|
React.createElement('div', { className: 'vsm-kb-label' },
|
|
270
250
|
React.createElement('span', null, label),
|
|
@@ -276,8 +256,8 @@ function PluginShortcutEditor({ service }) {
|
|
|
276
256
|
recordingRow
|
|
277
257
|
? React.createElement('button', { onClick: () => setRecording(null) }, '取消')
|
|
278
258
|
: React.createElement(React.Fragment, null,
|
|
279
|
-
React.createElement('button', { disabled: busy || config?.status !== 'ready', onClick: () => { setRecording(id); setMessage('') } }, '编辑'),
|
|
280
|
-
React.createElement('button', { disabled: busy || config?.status !== 'ready', title: '清除该命令的键位(恢复默认)', onClick: () => void saveBinding(id, null) }, '清除'))),
|
|
259
|
+
React.createElement('button', { disabled: busy || Boolean(recording) || config?.status !== 'ready', onClick: () => { setRecording(id); setMessage('') } }, '编辑'),
|
|
260
|
+
React.createElement('button', { disabled: busy || Boolean(recording) || config?.status !== 'ready', title: '清除该命令的键位(恢复默认)', onClick: () => void saveBinding(id, null) }, '清除'))),
|
|
281
261
|
)
|
|
282
262
|
})
|
|
283
263
|
|
|
@@ -289,6 +269,6 @@ function PluginShortcutEditor({ service }) {
|
|
|
289
269
|
React.createElement('span', { className: 'vsm-kb-dirty' }, recording ? '录制中…' : ''),
|
|
290
270
|
message && React.createElement('span', { className: 'vsm-kb-message' }, message),
|
|
291
271
|
error && React.createElement('span', { className: 'vsm-kb-warn' }, error),
|
|
292
|
-
React.createElement('button', { className: 'vsm-small', disabled: busy || config?.status !== 'ready', onClick: () => void resetAll() }, '恢复默认')),
|
|
272
|
+
React.createElement('button', { className: 'vsm-small', disabled: busy || Boolean(recording) || config?.status !== 'ready', onClick: () => void resetAll() }, '恢复默认')),
|
|
293
273
|
)
|
|
294
274
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* 与既有 `edrv:` 刷新/主题事件分属不同命名空间,互不干扰)。
|
|
7
7
|
* 作者 ddj 2026年09月10号
|
|
8
8
|
*/
|
|
9
|
+
import { panelAvailable, runPanelCommand } from '../panelCommands.js'
|
|
9
10
|
import { hasEditorModel, hasOpenTabs } from '../editorModelState.js'
|
|
10
11
|
import { activeEditorPath } from '../activePathStore.js'
|
|
11
12
|
import { isMarkdownPath } from '../markdownPreview.js'
|
|
@@ -201,8 +202,8 @@ export const EDITOR_COMMANDS: readonly CommandDef[] = [
|
|
|
201
202
|
},
|
|
202
203
|
{
|
|
203
204
|
id: 'edrv.quickOpen', label: '快速打开文件', category: '文件', order: 20,
|
|
204
|
-
available:
|
|
205
|
-
run: () =>
|
|
205
|
+
available: () => panelAvailable('quickOpen'),
|
|
206
|
+
run: () => { runPanelCommand('quickOpen') },
|
|
206
207
|
},
|
|
207
208
|
{
|
|
208
209
|
id: 'edrv.toggleSidebar', label: '切换侧边栏', category: '视图', order: 10,
|
|
@@ -211,8 +212,8 @@ export const EDITOR_COMMANDS: readonly CommandDef[] = [
|
|
|
211
212
|
},
|
|
212
213
|
{
|
|
213
214
|
id: 'edrv.searchInFiles', label: '在工作区中搜索', category: '视图', order: 20,
|
|
214
|
-
available:
|
|
215
|
-
run: () =>
|
|
215
|
+
available: () => panelAvailable('searchInFiles'),
|
|
216
|
+
run: () => { runPanelCommand('searchInFiles') },
|
|
216
217
|
},
|
|
217
218
|
{
|
|
218
219
|
id: 'edrv.toggleMarkdownPreview', label: '切换 Markdown 预览', category: '视图', order: 25,
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** Correlated editor readiness for external file opens. @author ddj 2026年09月28号 */
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { finishOpen, hasOpen, peekOpen } from './openReceipt.js'
|
|
4
|
+
import type { EditorRequest } from './openReceipt.js'
|
|
5
|
+
|
|
6
|
+
export interface ReceiptHost {
|
|
7
|
+
sessionId: string
|
|
8
|
+
open(request: EditorRequest): void | Promise<void>
|
|
9
|
+
ready(request: EditorRequest): boolean
|
|
10
|
+
error(request: EditorRequest): string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** One mounted editor owns at most one claimed external request. */
|
|
14
|
+
export class OpenMonitor {
|
|
15
|
+
private pending: EditorRequest | undefined
|
|
16
|
+
private frame: number | undefined
|
|
17
|
+
private stopped = false
|
|
18
|
+
private opening = false
|
|
19
|
+
/** @private @author ddj 2026年09月28号 Claim a request arriving after mount. */
|
|
20
|
+
private listener = (): void => { this.claim() }
|
|
21
|
+
|
|
22
|
+
/** @author ddj 2026年09月28号 @param read Read current component state without a stale closure. */
|
|
23
|
+
constructor(private readonly read: () => ReceiptHost) {}
|
|
24
|
+
|
|
25
|
+
/** @public @author ddj 2026年09月28号 Attach the listener and claim a pre-mount request. */
|
|
26
|
+
start(): void {
|
|
27
|
+
window.addEventListener('edrv:open-request', this.listener)
|
|
28
|
+
this.claim()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @public @author ddj 2026年09月28号 Fail claimed work and cancel the frame on unmount. */
|
|
32
|
+
stop(): void {
|
|
33
|
+
this.stopped = true
|
|
34
|
+
window.removeEventListener('edrv:open-request', this.listener)
|
|
35
|
+
if (this.frame !== undefined) cancelAnimationFrame(this.frame)
|
|
36
|
+
this.frame = undefined
|
|
37
|
+
if (this.pending) finishOpen(this.pending.requestId, false, '编辑器已关闭')
|
|
38
|
+
this.pending = undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @private @author ddj 2026年09月28号 Schedule at most one readiness/queue check. */
|
|
42
|
+
private arm(): void {
|
|
43
|
+
if (!this.stopped && this.frame === undefined) this.frame = requestAnimationFrame(this.check)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Settle only the request this instance still owns; queued work runs next frame.
|
|
48
|
+
* @private @author ddj 2026年09月28号
|
|
49
|
+
* @param request Claimed request, captured before asynchronous work.
|
|
50
|
+
* @param success Whether actual readiness was confirmed.
|
|
51
|
+
* @param error Failure description.
|
|
52
|
+
*/
|
|
53
|
+
private finish(request: EditorRequest, success: boolean, error?: string): void {
|
|
54
|
+
if (this.stopped || this.pending !== request) return
|
|
55
|
+
this.pending = undefined
|
|
56
|
+
this.opening = false
|
|
57
|
+
if (hasOpen(request.requestId)) finishOpen(request.requestId, success, error)
|
|
58
|
+
this.arm()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Observe startup without letting a late completion affect a new claim.
|
|
63
|
+
* @private @author ddj 2026年09月28号
|
|
64
|
+
* @param request Startup's request identity.
|
|
65
|
+
* @param operation Optional asynchronous editor startup.
|
|
66
|
+
*/
|
|
67
|
+
private watchOpen(request: EditorRequest, operation: void | Promise<void>): void {
|
|
68
|
+
if (!operation) return
|
|
69
|
+
this.opening = true
|
|
70
|
+
/** @author ddj 2026年09月28号 Release readiness only for this live claim. */
|
|
71
|
+
const ready = (): void => {
|
|
72
|
+
if (!this.stopped && this.pending === request) this.opening = false
|
|
73
|
+
}
|
|
74
|
+
/** @author ddj 2026年09月28号 @param error Correlated startup rejection. */
|
|
75
|
+
const fail = (error: unknown): void => this.finish(request, false, String(error))
|
|
76
|
+
void Promise.resolve(operation).then(ready, fail)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @private @author ddj 2026年09月28号 Claim this session; exceptions fail the exact request. */
|
|
80
|
+
private claim(): void {
|
|
81
|
+
if (this.stopped || (this.pending && hasOpen(this.pending.requestId))) return
|
|
82
|
+
this.pending = undefined
|
|
83
|
+
this.opening = false
|
|
84
|
+
try {
|
|
85
|
+
const host = this.read()
|
|
86
|
+
const request = peekOpen(host.sessionId)
|
|
87
|
+
if (!request) return
|
|
88
|
+
this.pending = request
|
|
89
|
+
this.watchOpen(request, host.open(request))
|
|
90
|
+
this.arm()
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (this.pending) this.finish(this.pending, false, String(error))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @private @author ddj 2026年09月28号 Check readiness; expired claims and exceptions cannot ACK later work. */
|
|
97
|
+
private check = (): void => {
|
|
98
|
+
this.frame = undefined
|
|
99
|
+
if (this.stopped) return
|
|
100
|
+
const request = this.pending
|
|
101
|
+
if (!request || !hasOpen(request.requestId)) { this.pending = undefined; this.claim(); return }
|
|
102
|
+
try {
|
|
103
|
+
const host = this.read()
|
|
104
|
+
if (request.sessionId !== host.sessionId) { this.finish(request, false, '会话已切换'); return }
|
|
105
|
+
if (this.opening) { this.arm(); return }
|
|
106
|
+
const error = host.error(request)
|
|
107
|
+
if (error || host.ready(request)) { this.finish(request, !error, error ?? undefined); return }
|
|
108
|
+
this.arm()
|
|
109
|
+
} catch (error) { this.finish(request, false, String(error)) }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Bind a component while reading its latest model state.
|
|
115
|
+
* @author ddj 2026年09月28号
|
|
116
|
+
* @param host Mounted editor actions and readiness checks.
|
|
117
|
+
*/
|
|
118
|
+
export function useOpenReceipt(host: ReceiptHost): void {
|
|
119
|
+
const latest = React.useRef(host)
|
|
120
|
+
latest.current = host
|
|
121
|
+
/** @author ddj 2026年09月28号 @returns Listener and animation-frame cleanup. */
|
|
122
|
+
function mount(): () => void {
|
|
123
|
+
const monitor = new OpenMonitor(() => latest.current)
|
|
124
|
+
monitor.start()
|
|
125
|
+
return () => monitor.stop()
|
|
126
|
+
}
|
|
127
|
+
React.useEffect(mount, [host.sessionId])
|
|
128
|
+
}
|
package/src/compat.ts
CHANGED
|
@@ -132,8 +132,10 @@ export function detectGuards(ctx: Ctx): CompatAdapter[] {
|
|
|
132
132
|
]
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/** 已实测覆盖的最高 DSH 版本(适配矩阵上界,超过则提示,见 buildReport)。
|
|
136
|
-
|
|
135
|
+
/** 已实测覆盖的最高 DSH 版本(适配矩阵上界,超过则提示,见 buildReport)。
|
|
136
|
+
* 0.1.7-rc.2 已在官方已签名打包 Desktop 上实机装配(认证路由、同源 RPC、项目 MCP
|
|
137
|
+
* 与私有打开队列均生效);更高版本仍按能力探测运行并提示。 */
|
|
138
|
+
const TESTED_DSH_MAX = '0.1.7-rc.2'
|
|
137
139
|
|
|
138
140
|
/** Config volatile 标记起效的版本线(此前设置走 section 安装,与 volatile 无关)。 */
|
|
139
141
|
const CONFIG_VOLATILE_MIN = '0.1.7-alpha.1'
|
|
@@ -210,7 +212,7 @@ export async function buildReport(
|
|
|
210
212
|
guards,
|
|
211
213
|
adapters,
|
|
212
214
|
warnings,
|
|
213
|
-
devForm: readDevForm(),
|
|
215
|
+
devForm: readDevForm(ctx),
|
|
214
216
|
dshVersion,
|
|
215
217
|
}
|
|
216
218
|
}
|
package/src/dap/manager.ts
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* 作者 ddj 2026年09月29号 / 2026年09月21号
|
|
19
19
|
*/
|
|
20
20
|
import { spawn, type ChildProcess } from 'node:child_process'
|
|
21
|
+
import { childEnv } from '../childEnv.js'
|
|
22
|
+
import { stopTree } from '../processTree.js'
|
|
21
23
|
import { join } from 'node:path'
|
|
22
24
|
import { createFrameParser, encodeMessage } from '../lsp/jsonrpc.js'
|
|
23
25
|
import { sourceBaseOf, sourceKeyOf, sourcePathOf, normalizeSourcePath } from './sourcePath.js'
|
|
@@ -288,9 +290,18 @@ export class DapSession {
|
|
|
288
290
|
|
|
289
291
|
// ---------------------------------------------------------------- 内部实现
|
|
290
292
|
|
|
291
|
-
/**
|
|
293
|
+
/**
|
|
294
|
+
* 启动独立适配器进程组并接入协议,清理隐式宿主凭据。
|
|
295
|
+
* @private
|
|
296
|
+
* @author ddj 2026年09月28号
|
|
297
|
+
* @param spec 适配器规格
|
|
298
|
+
* @throws spawn 同步失败
|
|
299
|
+
*/
|
|
292
300
|
private spawnAdapter(spec: DapAdapterSpec): void {
|
|
293
|
-
const child = spawn(spec.command, spec.args, {
|
|
301
|
+
const child = spawn(spec.command, spec.args, {
|
|
302
|
+
stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true,
|
|
303
|
+
detached: process.platform !== 'win32', env: this.adapterEnv(),
|
|
304
|
+
})
|
|
294
305
|
this.child = child
|
|
295
306
|
const parser = createFrameParser()
|
|
296
307
|
child.stdout?.on('data', (chunk: Buffer) => {
|
|
@@ -300,11 +311,16 @@ export class DapSession {
|
|
|
300
311
|
log.debug('[dap-adapter] ' + chunk.toString('utf8').trim())
|
|
301
312
|
this.stderrTail = (this.stderrTail + chunk.toString('utf8')).slice(-STDERR_TAIL_CAP)
|
|
302
313
|
})
|
|
303
|
-
child.on('error', (error) =>
|
|
314
|
+
child.on('error', (error) => {
|
|
315
|
+
if (this.child === child) this.fail('适配器进程错误:' + String(error))
|
|
316
|
+
})
|
|
317
|
+
child.stdin?.on('error', (error) => {
|
|
318
|
+
if (this.child === child) this.fail('适配器 stdin 错误:' + String(error))
|
|
319
|
+
})
|
|
304
320
|
child.on('exit', (code) => {
|
|
305
|
-
if (this.child
|
|
306
|
-
|
|
307
|
-
|
|
321
|
+
if (this.child !== child) return
|
|
322
|
+
if (this.phase === 'idle' || this.phase === 'terminated') this.killChild()
|
|
323
|
+
else this.fail('适配器退出(code=' + code + ')' + this.stderrSuffix())
|
|
308
324
|
})
|
|
309
325
|
}
|
|
310
326
|
|
|
@@ -529,18 +545,35 @@ export class DapSession {
|
|
|
529
545
|
this.setPhase('terminated')
|
|
530
546
|
}
|
|
531
547
|
|
|
532
|
-
/**
|
|
548
|
+
/**
|
|
549
|
+
* 清理父环境后合并 launch 配置显式 env,null 表示移除该项。
|
|
550
|
+
* @private
|
|
551
|
+
* @author ddj 2026年09月28号
|
|
552
|
+
* @returns 适配器的环境副本
|
|
553
|
+
*/
|
|
554
|
+
private adapterEnv(): NodeJS.ProcessEnv {
|
|
555
|
+
const explicit: NodeJS.ProcessEnv = {}
|
|
556
|
+
const raw = this.config?.raw?.env
|
|
557
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
558
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
559
|
+
if (typeof value === 'string' || value === null) explicit[key] = value ?? undefined
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return childEnv(explicit)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* 解除会话拥有权并终止适配器进程树;不先杀根进程。
|
|
567
|
+
* @private
|
|
568
|
+
* @author ddj 2026年09月28号
|
|
569
|
+
* @description 同时清理所有待处理请求与计时器。
|
|
570
|
+
*/
|
|
533
571
|
private killChild(): void {
|
|
534
572
|
const child = this.child
|
|
535
573
|
this.child = null
|
|
536
574
|
for (const entry of this.pending.values()) { clearTimeout(entry.timer); entry.resolve(null) }
|
|
537
575
|
this.pending.clear()
|
|
538
|
-
if (
|
|
539
|
-
try { child.kill() } catch { /* 忽略 */ }
|
|
540
|
-
if (process.platform === 'win32' && child.pid) {
|
|
541
|
-
try { spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }) } catch { /* 忽略 */ }
|
|
542
|
-
}
|
|
543
|
-
setTimeout(() => { try { child.kill('SIGKILL') } catch { /* 忽略 */ } }, 1500).unref?.()
|
|
576
|
+
if (child) stopTree(child, (line) => log.warn('[dap] ' + line))
|
|
544
577
|
}
|
|
545
578
|
|
|
546
579
|
/** 相位切换(running→running 等幂等跳过)。 */
|