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
@@ -1,8 +1,9 @@
1
1
  // @ts-nocheck
2
2
  /**
3
- * dsh-vscode-mode client — VSCodeMode 设置区:通用 / 快捷键 / MCP 管理 / 语言服务器 / 性能优化 / 兼容性。
3
+ * dsh-vscode-mode client — VSCodeMode 设置区:通用 / MCP 管理 / 语言服务器 / 性能优化 / 兼容性。
4
+ * (快捷键管理已迁出为独立顶层设置区 ui/ShortcutSettings.ts,与 VSCodeMode 平级。)
4
5
  * MCP 管理 Tab 下含三个子页签:我的 MCP(profile 全局)/ 项目 MCP(各项目 .mcp.json)/ MCP 市场(占位)。
5
- * 作者 ddj 2026年08月22号 / 2026年08月26号 / 2026年08月27号
6
+ * 作者 ddj 2026年08月22号 / 2026年08月26号 / 2026年08月27号 / 2026年10月
6
7
  */
7
8
  import React from 'react'
8
9
  import { rpc } from '../rpc.js'
@@ -14,7 +15,6 @@ import { normalizeSidebarMinWidth, SIDEBAR_MIN_DEFAULT } from '../sidebarMin.js'
14
15
  import { EDITOR_LIMIT_CEIL, EDITOR_LIMIT_DEFAULT, normalizeMaxOpenEditors } from '../../shared/editorLimit.js'
15
16
  import { TORTOISE_DIR_DEFAULT } from '../../shared/svn.js'
16
17
  import { DEFAULT_NATIVE_CSV } from '../../shared/nativeOpen.js'
17
- import { KeybindingsPanel } from './KeybindingsPanel.js'
18
18
  import { LspSettings } from './LspSettings.js'
19
19
  import { PerfSettings } from './PerfSettings.js'
20
20
  import { IntegrationSettings } from './IntegrationSettings.js'
@@ -44,9 +44,11 @@ function parsePairs(text) {
44
44
  }))
45
45
  }
46
46
 
47
- /** 服务状态文案。 */
47
+ /** @author ddj 2026年09月28号 @param server Server lifecycle snapshot. @returns User-facing status. */
48
48
  function statusOf(server) {
49
- return server.status === 'connected' ? '在线' : server.status === 'connecting' ? '连接中' : server.status === 'disabled' ? '已禁用' : '错误'
49
+ if (!server.enabled || server.status === 'disabled') return '已禁用'
50
+ const labels = { connected: '在线', connecting: '装配中', configured: '已配置,等待会话', unverified: '已加载,连接未确认', error: '错误' }
51
+ return labels[server.status] ?? '状态未知'
50
52
  }
51
53
 
52
54
  /** MCP 服务卡片(全局与项目共用)。 */
@@ -60,7 +62,8 @@ function ServerCard({ server, onRefresh, onToggle, onRemove }) {
60
62
  React.createElement('button', { className: 'vsm-switch ' + (server.enabled ? 'on' : ''), onClick: () => onToggle(server), 'aria-label': server.enabled ? '禁用' : '启用' }, server.enabled ? '●' : '○'),
61
63
  ),
62
64
  ),
63
- React.createElement('div', { className: 'vsm-mcp-meta' }, statusOf(server), ' · ', server.toolCount, ' 个工具 · ', server.transport),
65
+ React.createElement('div', { className: 'vsm-mcp-meta' }, statusOf(server), ' · ', server.toolCount, ' 个工具 · ', server.transport,
66
+ server.instanceCount !== undefined ? ' · ' + server.instanceCount + ' 个会话实例(各自连接)' : ''),
64
67
  server.error && React.createElement('div', { className: 'vsm-mcp-error' }, server.error),
65
68
  React.createElement('div', { className: 'vsm-mcp-tools' }, server.tools.map((tool) => React.createElement('span', { key: tool.name, className: 'vsm-mcp-chip', title: tool.description || tool.name }, tool.name))),
66
69
  )
@@ -581,7 +584,6 @@ export function McpSettings(props) {
581
584
  let body
582
585
  if (loading) body = React.createElement('div', { className: 'vsm-mcp-empty' }, '正在读取 MCP 服务…')
583
586
  else if (tab === 'general') body = React.createElement(GeneralSettings, { registry: openerRegistry })
584
- else if (tab === 'keys') body = React.createElement(KeybindingsPanel, null)
585
587
  else if (tab === 'mcp') body = React.createElement(McpManagePanel, { servers, projects, busy, draft, edit, resetDraft,
586
588
  saveGlobal: (close) => saveGlobal(close), saveProject: (workspacePath, close) => saveProject(workspacePath, close),
587
589
  refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject })
@@ -591,7 +593,7 @@ export function McpSettings(props) {
591
593
  else body = React.createElement(PerfSettings, null)
592
594
  return React.createElement('section', { className: 'vsm-mcp-page' },
593
595
  React.createElement('header', { className: 'vsm-mcp-header' }, React.createElement('div', null, React.createElement('h2', null, 'VSCodeMode'), React.createElement('p', null, '管理当前 profile 与各项目的 Model Context Protocol 服务。'))),
594
- React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === 'keys' ? 'active' : '', onClick: () => setTab('keys') }, '快捷键'), React.createElement('button', { className: tab === 'mcp' ? 'active' : '', onClick: () => setTab('mcp') }, 'MCP 管理'), React.createElement('button', { className: tab === 'lsp' ? 'active' : '', onClick: () => setTab('lsp') }, '语言服务器'), React.createElement('button', { className: tab === 'ai' ? 'active' : '', onClick: () => setTab('ai') }, 'AI 补全'), React.createElement('button', { className: tab === 'perf' ? 'active' : '', onClick: () => setTab('perf') }, '性能优化'), React.createElement('button', { className: tab === 'compat' ? 'active' : '', onClick: () => setTab('compat') }, '兼容性')),
596
+ React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === 'mcp' ? 'active' : '', onClick: () => setTab('mcp') }, 'MCP 管理'), React.createElement('button', { className: tab === 'lsp' ? 'active' : '', onClick: () => setTab('lsp') }, '语言服务器'), React.createElement('button', { className: tab === 'ai' ? 'active' : '', onClick: () => setTab('ai') }, 'AI 补全'), React.createElement('button', { className: tab === 'perf' ? 'active' : '', onClick: () => setTab('perf') }, '性能优化'), React.createElement('button', { className: tab === 'compat' ? 'active' : '', onClick: () => setTab('compat') }, '兼容性')),
595
597
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
596
598
  body,
597
599
  )
@@ -8,6 +8,7 @@
8
8
  * 作者 ddj 2026年09月09号 / 2026年09月10号
9
9
  */
10
10
  import React from 'react'
11
+ import { pickSessionScope } from '../sessionScope.js'
11
12
  import { EditorView } from './EditorView.js'
12
13
  import { setSideEditorMounted } from '../sidebarBridge.js'
13
14
  import { editorMountEpoch, markEditorActive, markEditorMounted, parseOfficialFileAddress, resolveNavLine, resolveNavOpen } from '../officialSidebar.js'
@@ -48,8 +49,10 @@ export function OfficialSideTab(props) {
48
49
  const schedule = props?.schedule
49
50
  const decide = () => {
50
51
  if (editorMountEpoch() !== epoch) return
51
- const current = props?.sessions?.list?.getSnapshot?.()?.current
52
- markEditorActive(current === self)
52
+ const current = typeof props?.readSessionScope === 'function'
53
+ ? props.readSessionScope().sessionId
54
+ : pickSessionScope({ list: props?.sessions?.list?.getSnapshot?.() }).sessionId
55
+ markEditorActive(Boolean(current && current !== self))
53
56
  }
54
57
  if (typeof schedule === 'function') schedule(decide, 0)
55
58
  else decide()
@@ -9,7 +9,8 @@
9
9
  */
10
10
  import React from 'react'
11
11
  import { rpc } from '../rpc.js'
12
- import { bindingsOf, chordOf, matchEvent, useKeybindingsVersion } from '../keybindings.js'
12
+ import { bindPanelCommand } from '../panelCommands.js'
13
+ import { ariaOf, chordOf, useKeybindingsVersion } from '../keybindings.js'
13
14
  import { stepIndex } from './searchNav.js'
14
15
 
15
16
  /**
@@ -64,22 +65,20 @@ export function QuickOpen(props) {
64
65
  inputRef.current?.focus?.()
65
66
  setOpen(true)
66
67
  }
67
- const onKey = (e) => {
68
- if (matchEvent(e, bindingsOf('edrv.quickOpen'))) {
69
- e.preventDefault(); e.stopPropagation()
70
- openBox()
71
- } else if (e.key === 'Escape') {
72
- setOpen(false); inputRef.current?.blur?.()
73
- }
68
+ // Esc 关闭(仅收起浮窗;历史 capture 行为保留,不参与命令派发、不吞键)
69
+ const onEscape = (e) => {
70
+ if (e.isComposing || e.keyCode === 229 || e.getModifierState?.('AltGraph') || e.key !== 'Escape') return
71
+ setOpen(false)
72
+ inputRef.current?.blur?.()
74
73
  }
75
- window.addEventListener('keydown', onKey, true)
76
- // 指令系统入口(命令栏「快速打开文件」):与键位复用同一动作
77
- window.addEventListener('edrv.command.quickOpen', openBox)
74
+ // 官方与旧版键位统一走会话级命令接收器;挂载时只消费本会话的待打开请求。
75
+ window.addEventListener('keydown', onEscape, true)
76
+ const offCommand = bindPanelCommand('quickOpen', { sessionId }, openBox)
78
77
  return () => {
79
- window.removeEventListener('keydown', onKey, true)
80
- window.removeEventListener('edrv.command.quickOpen', openBox)
78
+ window.removeEventListener('keydown', onEscape, true)
79
+ offCommand()
81
80
  }
82
- }, [])
81
+ }, [sessionId])
83
82
 
84
83
  // 高亮项滚入浮窗可视区(候选集或高亮变化时;nearest 避免整页跳动)
85
84
  React.useEffect(() => {
@@ -100,7 +99,7 @@ export function QuickOpen(props) {
100
99
  * @param e 输入框键盘事件
101
100
  */
102
101
  const onInputKey = (e) => {
103
- if (e.nativeEvent?.isComposing) return
102
+ if (e.nativeEvent?.isComposing || e.nativeEvent?.keyCode === 229 || e.getModifierState?.('AltGraph')) return
104
103
  if (e.key === 'Escape') { setOpen(false); return }
105
104
  if (!showPop || !list.length) return
106
105
  if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
@@ -136,7 +135,8 @@ export function QuickOpen(props) {
136
135
 
137
136
  return React.createElement('div', { className: 'edrv-search-wrap' },
138
137
  React.createElement('input', {
139
- ref: inputRef, className: 'edrv-search', placeholder: '搜索文件 (' + (chordOf('edrv.quickOpen') ?? 'Ctrl+P') + ')',
138
+ ref: inputRef, className: 'edrv-search', placeholder: '搜索文件' + (chordOf('edrv.quickOpen') ? ' (' + chordOf('edrv.quickOpen') + ')' : ''),
139
+ 'aria-keyshortcuts': ariaOf('edrv.quickOpen'),
140
140
  value: q,
141
141
  role: 'combobox',
142
142
  'aria-expanded': showPop && list.length > 0,
@@ -0,0 +1,274 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — 独立「快捷键」设置区(与 VSCodeMode 平级,settings.section slot)。
4
+ * 子页一「官方快捷键」:官方 shortcuts 目录只读总览(官方 + 插件命令)+ 一键打开官方快捷键弹窗;
5
+ * 子页二「插件快捷键」:插件命令(edrv.*)行内录键编辑,持久化经官方 edit API
6
+ * (Desktop: userData/keybindings.json;Web: localStorage dsh.keybindings.v1),不再写插件设置。
7
+ * 录制期间调 service.recording(true):Desktop 原生键盘桥暂停拦截,否则录不到键。
8
+ * 样式复用 vsm-kb-* / vsm-panel-* / vsm-mcp-subtabs(styles/mcp.css 随 client bundle 注入)。
9
+ * 作者 ddj 2026年10月
10
+ */
11
+ import React from 'react'
12
+ import { COMMANDS } from '../keybindings.js'
13
+ import { log } from '../log.js'
14
+ import { startRecording } from '../shortcutRecorder.js'
15
+ import { resetPluginKeys } from '../shortcutsOfficial.js'
16
+ import type { ShortcutsServiceLike, ShortcutCatalogRow } from '../shortcutsOfficial.js'
17
+
18
+ const settingsLog = log.child('shortcut-settings')
19
+
20
+ /** 目录行展示键位(keys 已按平台格式化;binding null = 未绑定)。 */
21
+ function keysText(row) {
22
+ if (!row || row.binding === null) return '未绑定'
23
+ return row.keys.join(' ')
24
+ }
25
+
26
+ /** 插件命令行 = COMMANDS 目录序 × 官方目录行(行缺失时占位「未注册」)。 */
27
+ function catalogRowsOf(service) {
28
+ const byId = new Map(service.catalog.getSnapshot().map((row) => [row.id, row]))
29
+ return COMMANDS.map((command) => ({ id: command.id, label: command.label, row: byId.get(command.id) ?? null }))
30
+ }
31
+
32
+ /**
33
+ * 「快捷键」设置区主体:子页切换 + 服务就绪探测(晚到自愈,有界重试)。
34
+ * @author ddj 2026年10月
35
+ * @param props.getShortcuts 官方服务运行时探测函数(client/index.ts 注入)
36
+ */
37
+ export function ShortcutSettings(props) {
38
+ const getShortcuts = props?.getShortcuts
39
+ const [service, setService] = React.useState(() => (typeof getShortcuts === 'function' ? getShortcuts() : null))
40
+ const [tab, setTab] = React.useState('official')
41
+ const [, setVersion] = React.useState(0)
42
+
43
+ // 服务就绪有界重试(服务晚到自愈;用尽保持提示文案)
44
+ React.useEffect(() => {
45
+ if (service) return undefined
46
+ let attempts = 0
47
+ let cancelled = false
48
+ const tick = () => {
49
+ if (cancelled) return
50
+ const found = typeof getShortcuts === 'function' ? getShortcuts() : null
51
+ if (found) {
52
+ setService(found)
53
+ return
54
+ }
55
+ if (attempts >= 15) return
56
+ attempts += 1
57
+ setTimeout(tick, 2000)
58
+ }
59
+ tick()
60
+ return () => {
61
+ cancelled = true
62
+ }
63
+ }, [service])
64
+
65
+ // 目录/配置变化即重渲染(官方 catalog/config 快照存储订阅)
66
+ React.useEffect(() => {
67
+ if (!service) return undefined
68
+ const bump = () => setVersion((v) => v + 1)
69
+ const offCatalog = service.catalog.subscribe(bump)
70
+ const offConfig = service.config.subscribe(bump)
71
+ return () => {
72
+ offCatalog()
73
+ offConfig()
74
+ }
75
+ }, [service])
76
+
77
+ let body
78
+ if (!service) {
79
+ body = React.createElement('div', { className: 'vsm-mcp-empty' },
80
+ '官方快捷键服务未就绪(正在探测;若长期无响应,说明当前 DSH 版本不含官方快捷键机制)。')
81
+ } else if (tab === 'plugin') {
82
+ body = React.createElement(PluginShortcutEditor, { service })
83
+ } else {
84
+ body = React.createElement(OfficialShortcutsPane, { service })
85
+ }
86
+
87
+ return React.createElement('section', { className: 'vsm-general-page' },
88
+ React.createElement('header', { className: 'vsm-mcp-header' }, React.createElement('div', null,
89
+ React.createElement('h2', null, '快捷键'),
90
+ React.createElement('p', null, '基于官方 shortcuts 机制:统一注册、冲突校验、按平台差异化默认值;Desktop 持久化到 userData/keybindings.json,Web 持久化到 localStorage(dsh.keybindings.v1)。'))),
91
+ React.createElement('nav', { className: 'vsm-mcp-subtabs' },
92
+ React.createElement('button', { className: tab === 'official' ? 'active' : '', onClick: () => setTab('official') }, '官方快捷键'),
93
+ React.createElement('button', { className: tab === 'plugin' ? 'active' : '', onClick: () => setTab('plugin') }, '插件快捷键')),
94
+ body,
95
+ )
96
+ }
97
+
98
+ /** 官方目录行(只读总览行)。 */
99
+ function OfficialRow({ row }) {
100
+ return React.createElement('div', { className: 'vsm-kb-row' },
101
+ React.createElement('div', { className: 'vsm-kb-label' },
102
+ React.createElement('span', null, row.label),
103
+ React.createElement('small', null, row.id),
104
+ row.modified && React.createElement('small', { className: 'vsm-kb-warn' }, '已自定义'),
105
+ row.issue && React.createElement('small', { className: 'vsm-kb-warn' }, '键位不受当前环境支持'),
106
+ row.conflicts.length > 0 && React.createElement('small', { className: 'vsm-kb-warn' }, '与 ' + row.conflicts.join('、') + ' 冲突')),
107
+ React.createElement('kbd', { 'aria-label': row?.aria, className: 'vsm-kb-key' + (row.conflicts.length ? ' warn' : '') }, keysText(row)),
108
+ )
109
+ }
110
+
111
+ /** 子页一:官方快捷键(弹窗入口 + 目录总览)。 */
112
+ function OfficialShortcutsPane({ service }) {
113
+ const [error, setError] = React.useState('')
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(' ') + ')' : ''
117
+ const config = service.config.getSnapshot()
118
+ const openOfficial = () => {
119
+ setError('')
120
+ if (!service.registry || typeof service.registry.invoke !== 'function') {
121
+ setError('官方弹窗入口不可用;可到 设置 → 通用 → 快捷键 打开')
122
+ return
123
+ }
124
+ try {
125
+ service.registry.invoke('shortcuts.open', {
126
+ region: 'page',
127
+ modal: null,
128
+ target: typeof document !== 'undefined' ? document.activeElement : null,
129
+ })
130
+ } catch (e) {
131
+ settingsLog.warn('打开官方快捷键弹窗失败:' + String(e))
132
+ setError('打开官方弹窗失败:' + String(e) + ';可到 设置 → 通用 → 快捷键 打开')
133
+ }
134
+ }
135
+ return React.createElement(React.Fragment, null,
136
+ React.createElement('section', { className: 'vsm-panel' },
137
+ React.createElement('h3', { className: 'vsm-panel-title' }, '官方快捷键配置'),
138
+ React.createElement('div', { className: 'vsm-panel-body' },
139
+ React.createElement('p', null, '官方快捷键弹窗提供全部命令的查看、录键与恢复默认;也可从 设置 → 通用 → 快捷键 打开。下方为当前目录只读总览(含插件注册的命令)。'),
140
+ React.createElement('button', { className: 'vsm-primary vsm-small', onClick: openOfficial, 'aria-keyshortcuts': shortcut?.aria }, '打开官方快捷键设置' + hint),
141
+ error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
142
+ config?.status === 'unreadable' && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' },
143
+ '快捷键配置读取失败(内容损坏或由更新版本写入),当前使用默认键位;请备份后清理配置,或升级 DSH。')),
144
+ ),
145
+ React.createElement('section', { className: 'vsm-panel' },
146
+ React.createElement('h3', { className: 'vsm-panel-title' }, '命令目录(只读总览)'),
147
+ React.createElement('div', { className: 'vsm-panel-body' },
148
+ React.createElement('div', { className: 'vsm-kb-list' }, rows.map((row) => React.createElement(OfficialRow, { key: row.id, row }))),
149
+ ),
150
+ ),
151
+ )
152
+ }
153
+
154
+ /** 子页二:插件命令键位(行内录键 + 行级清除 + 恢复默认;写入走官方 edit API)。 */
155
+ function PluginShortcutEditor({ service }) {
156
+ const [nativeReady, setNativeReady] = React.useState(false)
157
+ const [recording, setRecording] = React.useState(null) // 正在录制的命令 id
158
+ const [busy, setBusy] = React.useState(false)
159
+ const [message, setMessage] = React.useState('')
160
+ const [error, setError] = React.useState('')
161
+ const rows = catalogRowsOf(service)
162
+ const config = service.config.getSnapshot()
163
+
164
+ const saveBinding = async (id, binding) => {
165
+ setBusy(true)
166
+ setError('')
167
+ setMessage('')
168
+ const buildEdit = () => (binding === null ? { type: 'reset', id } : { type: 'set', id, binding })
169
+ try {
170
+ const result = await service.edit(buildEdit(), service.config.getSnapshot().revision)
171
+ if (result.status === 'saved') {
172
+ setMessage('已保存')
173
+ } else if (result.status === 'stale') {
174
+ setError('快捷键配置已在其他窗口更新,请检查当前键位后重新录制')
175
+ } else if (result.status === 'conflict') {
176
+ // 官方按原因分类拒绝:issue = 键位本身不合法(如 web 白名单外),conflicts = 与其他命令撞键
177
+ if (result.issue) {
178
+ const reasons = {
179
+ 'unsupported-browser': 'web 模式白名单外的组合(单修饰符组合在浏览器环境不可靠)',
180
+ 'reserved': '官方保留键',
181
+ 'unsupported-key': '官方不支持的键',
182
+ 'modifier-required': '至少需要一个修饰键',
183
+ }
184
+ setError('键位被官方拒绝:' + (reasons[result.issue] ?? result.issue)
185
+ + '。web 模式可用 Ctrl+Alt+X 风格组合;升级桌面外壳进入桌面模式后不受此限。')
186
+ } else {
187
+ setError('键位冲突:与 ' + (result.conflicts?.join('、') ?? '其他命令') + ' 冲突,请换一个组合')
188
+ }
189
+ } else if (result.status === 'not-ready') {
190
+ setError('快捷键配置尚未就绪,请稍后重试')
191
+ } else if (result.status === 'unreadable') {
192
+ setError('快捷键配置读取失败,编辑已禁用(见官方提示)')
193
+ } else {
194
+ setError('写入失败,请重试')
195
+ }
196
+ } catch (e) {
197
+ settingsLog.warn('快捷键保存失败:' + String(e))
198
+ setError('保存失败:' + String(e))
199
+ } finally {
200
+ setBusy(false)
201
+ }
202
+ }
203
+
204
+ const resetAll = async () => {
205
+ if (!window.confirm('恢复全部插件快捷键为默认值?(只影响本插件命令的键位覆盖)')) return
206
+ setBusy(true)
207
+ setError('')
208
+ setMessage('')
209
+ try {
210
+ const result = await resetPluginKeys(service)
211
+ if (result.failed) setError('已恢复 ' + result.reset + ' 条,' + result.failed + ' 恢复失败;请重试')
212
+ else setMessage('已恢复 ' + result.reset + ' 条插件快捷键')
213
+ } catch (e) {
214
+ settingsLog.warn('快捷键恢复默认失败:' + String(e))
215
+ setError('恢复失败:' + String(e))
216
+ } finally {
217
+ setBusy(false)
218
+ }
219
+ }
220
+
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 = () => {
227
+ if (!recording) return undefined
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])
242
+
243
+ const rowsView = rows.map(({ id, label, row }) => {
244
+ const recordingRow = recording === id
245
+ const display = recordingRow
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))
248
+ return React.createElement('div', { key: id, className: 'vsm-kb-row' + (recordingRow ? ' rec' : '') },
249
+ React.createElement('div', { className: 'vsm-kb-label' },
250
+ React.createElement('span', null, label),
251
+ row === null && React.createElement('small', { className: 'vsm-kb-warn' }, '未注册(官方服务未同步)'),
252
+ row && row.modified && React.createElement('small', { className: 'vsm-kb-warn' }, '已自定义'),
253
+ row && row.conflicts.length > 0 && React.createElement('small', { className: 'vsm-kb-warn' }, '与 ' + row.conflicts.join('、') + ' 冲突')),
254
+ display,
255
+ React.createElement('div', { className: 'vsm-kb-actions' },
256
+ recordingRow
257
+ ? React.createElement('button', { onClick: () => setRecording(null) }, '取消')
258
+ : React.createElement(React.Fragment, 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) }, '清除'))),
261
+ )
262
+ })
263
+
264
+ return React.createElement('section', { className: 'vsm-panel' },
265
+ React.createElement('h3', { className: 'vsm-panel-title' }, '插件命令键位'),
266
+ React.createElement('div', { className: 'vsm-panel-body' },
267
+ React.createElement('div', { className: 'vsm-kb-list' }, rowsView)),
268
+ React.createElement('div', { className: 'vsm-panel-body vsm-kb-actionsbar' },
269
+ React.createElement('span', { className: 'vsm-kb-dirty' }, recording ? '录制中…' : ''),
270
+ message && React.createElement('span', { className: 'vsm-kb-message' }, message),
271
+ error && React.createElement('span', { className: 'vsm-kb-warn' }, error),
272
+ React.createElement('button', { className: 'vsm-small', disabled: busy || Boolean(recording) || config?.status !== 'ready', onClick: () => void resetAll() }, '恢复默认')),
273
+ )
274
+ }
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * dsh-vscode-mode client — 编辑器指令目录(纯元数据)。
3
- * 「一条命令 = 一条注册数据」:命令栏、快捷键设置页、快捷键监听与 Monaco 右键菜单
4
- * 全部从本表读取;新增一条能力只需在 EDITOR_COMMANDS 追加一项(可选 keybinding)。
3
+ * 「一条命令 = 一条注册数据」:命令栏、快捷键设置页与 Monaco 右键菜单
4
+ * 全部从本表读取;新增一条能力只需在 EDITOR_COMMANDS 追加一项(默认键位见 shared/keybindings SHORTCUT_PROFILES)。
5
5
  * run 只派发 `edrv.command.*` 窗口事件,不直接触碰 React/Monaco(`edrv.command.` 前缀
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'
11
+ import { activeEditorPath } from '../activePathStore.js'
12
+ import { isMarkdownPath } from '../markdownPreview.js'
10
13
  import { svnCurrentStatus } from '../svnStatus.js'
11
14
  import { dapStore } from '../dap/store.js'
12
15
  import { svnActionOn, svnActionsFor } from '../../shared/svnActions.js'
@@ -22,8 +25,6 @@ export interface CommandDef {
22
25
  category: string
23
26
  /** 组内排序(小者优先)。 */
24
27
  order?: number
25
- /** 默认键位弦(可选;可含 `|` 多候选)。声明后由快捷键设置页展示与录制。 */
26
- keybinding?: string
27
28
  /** 运行体:派发窗口事件或直接执行。 */
28
29
  run: () => void
29
30
  /** 可用性判定(缺省视为始终可用;返回 false 时命令栏隐藏且 run 被拒)。 */
@@ -62,7 +63,7 @@ function alwaysAvailable(): boolean {
62
63
  function nextEditorRowDef(): CommandDef {
63
64
  return {
64
65
  id: 'edrv.nextEditorRow', label: '下一编辑行(光标整行下移)', category: '导航', order: 50,
65
- keybinding: 'Ctrl+Alt+ArrowDown', available: needsModel,
66
+ available: needsModel,
66
67
  run: () => emit('nextEditorRow'),
67
68
  }
68
69
  }
@@ -75,7 +76,7 @@ function nextEditorRowDef(): CommandDef {
75
76
  function prevEditorRowDef(): CommandDef {
76
77
  return {
77
78
  id: 'edrv.prevEditorRow', label: '上一编辑行(光标整行上移)', category: '导航', order: 60,
78
- keybinding: 'Ctrl+Alt+ArrowUp', available: needsModel,
79
+ available: needsModel,
79
80
  run: () => emit('prevEditorRow'),
80
81
  }
81
82
  }
@@ -88,7 +89,7 @@ function prevEditorRowDef(): CommandDef {
88
89
  function gotoLineDef(): CommandDef {
89
90
  return {
90
91
  id: 'edrv.goToLine', label: '转到行', category: '导航', order: 45,
91
- keybinding: 'Ctrl+G', available: needsModel,
92
+ available: needsModel,
92
93
  run: () => emit('goToLine'),
93
94
  }
94
95
  }
@@ -102,7 +103,7 @@ function gotoLineDef(): CommandDef {
102
103
  function addSelectionRefDef(): CommandDef {
103
104
  return {
104
105
  id: 'edrv.addSelectionRef', label: '添加选中内容为引用', category: '编辑', order: 10,
105
- keybinding: 'Ctrl+U', available: needsModel,
106
+ available: needsModel,
106
107
  run: () => emit('addSelectionRef'),
107
108
  }
108
109
  }
@@ -117,7 +118,7 @@ function addSelectionRefDef(): CommandDef {
117
118
  function closeTabDef(): CommandDef {
118
119
  return {
119
120
  id: 'edrv.closeTab', label: '关闭当前页签', category: '文件', order: 40,
120
- keybinding: 'Ctrl+F4', available: hasOpenTabs,
121
+ available: hasOpenTabs,
121
122
  run: () => emit('closeTab'),
122
123
  }
123
124
  }
@@ -188,34 +189,39 @@ function svnEventOf(action: SvnActionDef): string {
188
189
 
189
190
  /**
190
191
  * 编辑器内置指令目录(顺序 = 快捷键设置页展示顺序)。
191
- * 前置 8 条的键位由 EditorView / QuickOpen 自行 capture 监听(历史实现),
192
- * 故不进 BRIDGE_COMMANDS,避免同一按键双执行。
193
- * @author ddj 2026年09月10号
192
+ * 键位派发已归官方 shortcuts 机制:默认键位表在 shared/keybindings.ts 的 SHORTCUT_PROFILES,
193
+ * 由 client/index.ts 经 shortcutsOfficial.registerOfficialShortcuts 注册,本目录只管
194
+ * 「标签/可用性/执行」。无默认键位的命令同样注册(官方 UI/插件子页内可自定义绑定)。
195
+ * @author ddj 2026年09月10号 / 2026年10月
194
196
  */
195
197
  export const EDITOR_COMMANDS: readonly CommandDef[] = [
196
198
  {
197
199
  id: 'edrv.save', label: '保存文件', category: '文件', order: 10,
198
- keybinding: 'Ctrl+S', available: needsModel,
200
+ available: needsModel,
199
201
  run: () => emit('save'),
200
202
  },
201
203
  {
202
204
  id: 'edrv.quickOpen', label: '快速打开文件', category: '文件', order: 20,
203
- keybinding: 'Ctrl+P', available: alwaysAvailable,
204
- run: () => emit('quickOpen'),
205
+ available: () => panelAvailable('quickOpen'),
206
+ run: () => { runPanelCommand('quickOpen') },
205
207
  },
206
208
  {
207
209
  id: 'edrv.toggleSidebar', label: '切换侧边栏', category: '视图', order: 10,
208
- keybinding: 'Ctrl+B', available: alwaysAvailable,
210
+ available: alwaysAvailable,
209
211
  run: () => emit('toggleSidebar'),
210
212
  },
211
213
  {
212
214
  id: 'edrv.searchInFiles', label: '在工作区中搜索', category: '视图', order: 20,
213
- keybinding: 'Ctrl+Shift+F', available: alwaysAvailable,
214
- run: () => emit('searchInFiles'),
215
+ available: () => panelAvailable('searchInFiles'),
216
+ run: () => { runPanelCommand('searchInFiles') },
215
217
  },
216
218
  {
217
219
  id: 'edrv.toggleMarkdownPreview', label: '切换 Markdown 预览', category: '视图', order: 25,
218
- keybinding: 'Ctrl+Shift+V', available: alwaysAvailable,
220
+ // 仅活动文件是 .md 时可用(不可用时官方派发返回 pass 不吞键,保留浏览器「无格式粘贴」语义)
221
+ available: () => {
222
+ const path = activeEditorPath()
223
+ return typeof path === 'string' && isMarkdownPath(path)
224
+ },
219
225
  run: () => emit('toggleMarkdownPreview'),
220
226
  },
221
227
  {
@@ -225,22 +231,22 @@ export const EDITOR_COMMANDS: readonly CommandDef[] = [
225
231
  },
226
232
  {
227
233
  id: 'edrv.navigateBack', label: '后退(导航历史)', category: '导航', order: 10,
228
- keybinding: 'Alt+ArrowLeft|Ctrl+Alt+-', available: needsModel,
234
+ available: needsModel,
229
235
  run: () => emit('navigateBack'),
230
236
  },
231
237
  {
232
238
  id: 'edrv.navigateForward', label: '前进(导航历史)', category: '导航', order: 20,
233
- keybinding: 'Alt+ArrowRight|Ctrl+Shift+-', available: needsModel,
239
+ available: needsModel,
234
240
  run: () => emit('navigateForward'),
235
241
  },
236
242
  {
237
243
  id: 'edrv.nextTab', label: '下一个页签', category: '导航', order: 30,
238
- keybinding: 'Ctrl+Alt+ArrowRight|Ctrl+PageDown', available: needsModel,
244
+ available: needsModel,
239
245
  run: () => emit('nextTab'),
240
246
  },
241
247
  {
242
248
  id: 'edrv.prevTab', label: '上一个页签', category: '导航', order: 40,
243
- keybinding: 'Ctrl+Alt+ArrowLeft|Ctrl+PageUp', available: needsModel,
249
+ available: needsModel,
244
250
  run: () => emit('prevTab'),
245
251
  },
246
252
  {
@@ -291,41 +297,41 @@ function debugPaletteDefs(): CommandDef[] {
291
297
  return [
292
298
  {
293
299
  id: 'edrv.debugToggleBreakpoint', label: '调试:切换断点(光标行)', category: '调试', order: 10,
294
- keybinding: 'F9', available: needsModel,
300
+ available: needsModel,
295
301
  run: () => emit('debugToggleBreakpoint'),
296
302
  },
297
303
  {
298
304
  id: 'edrv.debugStartContinue', label: '调试:启动 / 继续', category: '调试', order: 20,
299
- keybinding: 'F5', available: canStart,
305
+ available: canStart,
300
306
  run: () => emit('debugStartContinue'),
301
307
  },
302
308
  {
303
309
  id: 'edrv.debugStepOver', label: '调试:单步跳过', category: '调试', order: 30,
304
- keybinding: 'F10', available: paused,
310
+ available: paused,
305
311
  run: () => emit('debugStepOver'),
306
312
  },
307
313
  {
308
314
  id: 'edrv.debugStepInto', label: '调试:单步步入', category: '调试', order: 40,
309
- keybinding: 'F11', available: paused,
315
+ available: paused,
310
316
  run: () => emit('debugStepInto'),
311
317
  },
312
318
  {
313
319
  id: 'edrv.debugStepOut', label: '调试:单步步出', category: '调试', order: 50,
314
- keybinding: 'Shift+F11', available: paused,
320
+ available: paused,
315
321
  run: () => emit('debugStepOut'),
316
322
  },
317
323
  {
318
324
  id: 'edrv.debugStop', label: '调试:停止', category: '调试', order: 60,
319
- keybinding: 'Shift+F5', available: canStop,
325
+ available: canStop,
320
326
  run: () => emit('debugStop'),
321
327
  },
322
328
  ]
323
329
  }
324
330
 
325
331
  /**
326
- * 桥接派发指令(无原生监听,键位由 commandBridge 统一 capture 处理)。
327
- * 新增「只填目录、不写监听」的编辑器指令一律放这里:键位、命令栏、设置页自动可用。
328
- * @author ddj 2026年09月10号
332
+ * 非编辑器原生路径的指令(键位统一由官方 shortcuts 机制派发;历史「桥接」分组名保留)。
333
+ * 新增「只填目录」的编辑器指令一律放这里:命令栏与官方快捷键目录自动可用。
334
+ * @author ddj 2026年09月10号 / 2026年10月
329
335
  */
330
336
  export const BRIDGE_COMMANDS: readonly CommandDef[] = [
331
337
  nextEditorRowDef(),
@@ -345,16 +351,6 @@ export const BRIDGE_COMMANDS: readonly CommandDef[] = [
345
351
  export function showCommandsDef(run: () => void): CommandDef {
346
352
  return {
347
353
  id: 'edrv.showCommands', label: '显示所有命令', category: '视图', order: 1,
348
- keybinding: 'Ctrl+Shift+P|F1', available: alwaysAvailable, run,
354
+ available: alwaysAvailable, run,
349
355
  }
350
356
  }
351
-
352
- /**
353
- * 需要全局键位派发的指令(桥接类 + 命令栏自身;不含 EditorView/QuickOpen 原生监听的那些)。
354
- * @author ddj 2026年09月10号
355
- * @param showCommands 命令栏命令定义
356
- * @returns 派发集合
357
- */
358
- export function dispatchedCommands(showCommands: CommandDef): readonly CommandDef[] {
359
- return [...BRIDGE_COMMANDS, showCommands]
360
- }