dsh-vscode-mode 0.14.2 → 0.14.4

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.
@@ -11,7 +11,7 @@ import React from 'react'
11
11
  import { useOpenReceipt } from '../useOpenReceipt.js'
12
12
  import { acceptsLoad, receiptErrorFor, receiptPath, receiptReadyFor, receiptTabPath } from '../receiptState.js'
13
13
  import { bindPanelCommand } from '../panelCommands.js'
14
- import { dbg, rpc } from '../rpc.js'
14
+ import { dbg, isDebug, rpc } from '../rpc.js'
15
15
  import { emitFileChanged, emitRefresh } from '../events.js'
16
16
  import { langOf, loadMonaco, snippetLanguageOf } from '../monaco/loader.js'
17
17
  import { clampZoom, clearZoomMem, dataUrlOf, isImagePath, isSvgPath, recallZoom, rememberZoom, zoomKeyOf, zoomStepOf } from '../imagePreview.js'
@@ -44,7 +44,8 @@ import { CommandPalette } from './CommandPalette.js'
44
44
  import { closeCommandPalette } from '../commandPaletteStore.js'
45
45
  import { DiffLauncher } from './DiffLauncher.js'
46
46
  import { SidebarView } from '../sidebar/SidebarView.js'
47
- import { clearDiffDock, publishDiffDock } from '../diffDockStore.js'
47
+ import { clearDiffDock, dockInLayout, publishDiffDock } from '../diffDockStore.js'
48
+ import { DiffBox } from './DiffBox.js'
48
49
  import { displayDiffTotal, editorDockMode } from '../diffDock.js'
49
50
  import { editorHeight } from '../editorLayout.js'
50
51
  import { revealInExplorer as revealPathInExplorer } from '../fileReveal.js'
@@ -479,7 +480,19 @@ export function EditorView(props) {
479
480
  // 当前 tab 是否为可预览的 Markdown,以及是否正处于预览态(预览态替换 Monaco host,只读)
480
481
  const isMdActive = !!active && isMarkdownPath(active)
481
482
  const mdPreviewing = isMdActive && mdPreviewRef.current.has(active)
482
- const regions = React.useMemo(() => diffRegions(currentRecords, contentReady ? content : null).filter((r) => !r.superseded), [currentRecords, content, contentPath, active])
483
+ const regions = React.useMemo(() => {
484
+ // #region debug log
485
+ const trace = isDebug() && contentReady && currentRecords.length > 0
486
+ const start = trace ? performance.now() : 0
487
+ // #endregion
488
+ const next = diffRegions(currentRecords, contentReady ? content : null).filter((r) => !r.superseded)
489
+ // #region debug log
490
+ if (trace) {
491
+ try { dbg(sessionId, `[DEBUG diffRegions] records=${currentRecords.length} bytes=${content.length} regions=${next.length} ms=${(performance.now() - start).toFixed(1)}`) } catch (e) { /* 诊断不可影响渲染 */ }
492
+ }
493
+ // #endregion
494
+ return next
495
+ }, [currentRecords, content, contentPath, active])
483
496
  // useMemo 稳定引用:否则 hover 等重渲染会让 view zone effect 反复重建(- 号闪烁)
484
497
  const pendingRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && !r.stale), [regions])
485
498
  const staleRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && r.stale), [regions])
@@ -802,14 +815,35 @@ export function EditorView(props) {
802
815
 
803
816
  const refreshRecords = (skipStale) => {
804
817
  if (!sessionId) return
818
+ // #region debug log
819
+ const trace = isDebug()
820
+ const start = trace ? performance.now() : 0
821
+ // #endregion
805
822
  rpc('edrv.list', { sessionId, ...(skipStale ? { skipStale: true } : {}) }).then((res) => {
823
+ // #region debug log
824
+ const received = trace ? performance.now() : 0
825
+ // #endregion
806
826
  if (!res || !res.ok || !Array.isArray(res.records)) return
807
827
  const map = {}
808
828
  for (const r of res.records) {
809
829
  if (r.archived === true) continue
810
830
  map[r.callId] = r
811
831
  }
812
- setRecords((prev) => (sameRecords(prev, map) ? prev : map))
832
+ setRecords((prev) => {
833
+ // #region debug log
834
+ const compareStart = trace ? performance.now() : 0
835
+ // #endregion
836
+ const equal = sameRecords(prev, map)
837
+ // #region debug log
838
+ if (trace) {
839
+ try {
840
+ const compareMs = performance.now() - compareStart
841
+ if (compareMs > 20 || received - start > 100) dbg(sessionId, `[DEBUG diffList] count=${res.records.length} skipStale=${skipStale === true} rpcMs=${(received - start).toFixed(1)} compareMs=${compareMs.toFixed(1)} changed=${!equal}`)
842
+ } catch (e) { /* 诊断不可影响轮询 */ }
843
+ }
844
+ // #endregion
845
+ return equal ? prev : map
846
+ })
813
847
  }).catch((e) => setError('list异常:' + String(e)))
814
848
  }
815
849
 
@@ -942,7 +976,15 @@ export function EditorView(props) {
942
976
  return
943
977
  }
944
978
  setLoadStage({ progress: monaco ? 72 : 12, message: '读取文件内容…' })
979
+ // #region debug log
980
+ const readStart = isDebug() ? performance.now() : 0
981
+ // #endregion
945
982
  rpc('edrv.read', { sessionId: sid, path }).then((res) => {
983
+ // #region debug log
984
+ if (readStart) {
985
+ try { dbg(sid, `[DEBUG diffRead] force=${force === true} ok=${res?.ok === true} bytes=${typeof res?.content === 'string' ? res.content.length : 0} rpcMs=${(performance.now() - readStart).toFixed(1)}`) } catch (e) { /* 诊断不可影响文件读取 */ }
986
+ }
987
+ // #endregion
946
988
  if (!loadCurrent(path, seq)) return
947
989
  if (res && res.ok) {
948
990
  mark(res.version, true)
@@ -2704,7 +2746,20 @@ export function EditorView(props) {
2704
2746
  */
2705
2747
  const actHunk = (region, reject, silent) => {
2706
2748
  const method = reject ? 'edrv.reject' : 'edrv.accept'
2749
+ // #region debug log
2750
+ const trace = isDebug()
2751
+ const start = trace ? performance.now() : 0
2752
+ const traceId = trace ? `${Date.now()}-${Math.floor(Math.random() * 1000)}` : ''
2753
+ if (trace) {
2754
+ try { dbg(sessionId, `[DEBUG diffAction] stage=start trace=${traceId} method=${method} scope=${region.create ? 'call' : 'hunk'}`) } catch (e) { /* 诊断不可影响决策 */ }
2755
+ }
2756
+ // #endregion
2707
2757
  return rpc(method, { sessionId, callId: region.callId, scope: region.create ? 'call' : 'hunk', hunkIndex: region.idx }).then((res) => {
2758
+ // #region debug log
2759
+ if (trace) {
2760
+ try { dbg(sessionId, `[DEBUG diffAction] stage=reply trace=${traceId} method=${method} ok=${res?.ok === true} rpcMs=${(performance.now() - start).toFixed(1)}`) } catch (e) { /* 诊断不可影响决策 */ }
2761
+ }
2762
+ // #endregion
2708
2763
  if (res && res.ok) {
2709
2764
  setRecords((prev) => Object.assign({}, prev, { [region.callId]: res.record }))
2710
2765
  if (!silent) { reloadFile(); emitRefresh() }
@@ -2727,7 +2782,21 @@ export function EditorView(props) {
2727
2782
  */
2728
2783
  const actMany = (items) => {
2729
2784
  if (!items.length) return Promise.resolve({ ok: 0, fail: 0, stale: 0 })
2785
+ // #region debug log
2786
+ const trace = isDebug()
2787
+ const start = trace ? performance.now() : 0
2788
+ const rejected = trace ? items.filter((item) => item.decision === 'rejected').length : 0
2789
+ const traceId = trace ? `${Date.now()}-${Math.floor(Math.random() * 1000)}` : ''
2790
+ if (trace) {
2791
+ try { dbg(sessionId, `[DEBUG diffAction] stage=start trace=${traceId} method=decideBatch items=${items.length} rejected=${rejected}`) } catch (e) { /* 诊断不可影响决策 */ }
2792
+ }
2793
+ // #endregion
2730
2794
  return rpc('edrv.decideBatch', { sessionId, items }).then((res) => {
2795
+ // #region debug log
2796
+ if (trace) {
2797
+ try { dbg(sessionId, `[DEBUG diffAction] stage=reply trace=${traceId} method=decideBatch items=${items.length} rpcMs=${(performance.now() - start).toFixed(1)} ok=${res?.ok === true}`) } catch (e) { /* 诊断不可影响决策 */ }
2798
+ }
2799
+ // #endregion
2731
2800
  if (!res || !res.ok || !Array.isArray(res.results)) {
2732
2801
  setError(res?.error ? String(res.error) : '批量处理失败')
2733
2802
  return { ok: 0, fail: items.length, stale: 0 }
@@ -3700,9 +3769,14 @@ export function EditorView(props) {
3700
3769
  React.createElement('button', { className: 'edrv-pill edrv-pill-undo', onClick: () => { dismissHover(); actHunk(hoverAct.region, true) } }, '↩ Undo'))
3701
3770
  : null
3702
3771
 
3703
- /** 组装差异 dock 快照(发布给 conversation.input.dock 的唯一 DiffBox 实例)。 */
3772
+ /**
3773
+ * 组装当前编辑器的差异上下文,供侧栏本地与会话 dock 共用。
3774
+ * @author ddj 2026年09月29号
3775
+ * @returns 含布局及操作回调的差异快照
3776
+ */
3704
3777
  const buildDockSnapshot = () => ({
3705
3778
  mode: editorDockMode(active),
3779
+ layout,
3706
3780
  pendingRegions,
3707
3781
  staleRegions,
3708
3782
  onAct: actHunk,
@@ -3729,9 +3803,10 @@ export function EditorView(props) {
3729
3803
  onOpenNextFile: openNextFile,
3730
3804
  })
3731
3805
 
3806
+ const dockSnapshot = buildDockSnapshot()
3732
3807
  React.useEffect(() => {
3733
3808
  if (!sessionId) return
3734
- publishDiffDock(sessionId, buildDockSnapshot(), dockSourceRef.current)
3809
+ publishDiffDock(sessionId, dockSnapshot, dockSourceRef.current)
3735
3810
  // 侧栏形态同步 Tab 角标计数(仅本形态写入;页签形态清零避免残留)
3736
3811
  if (layout === 'side') setSidePending(sessionId, sum.totalFiles)
3737
3812
  else setSidePending(sessionId, 0)
@@ -3930,12 +4005,20 @@ export function EditorView(props) {
3930
4005
  }, '添加配置…')
3931
4006
  : null
3932
4007
 
3933
- // 主编辑列(侧边栏右侧):pathBar + tabRow + 编辑/差异区(底部整条留给 DSH 对话输入栏)
4008
+ // 侧栏差异栏悬浮在编辑区底部;中央页签仍使用对话 dock。
4009
+ const sideDock = dockInLayout(dockSnapshot, 'side')
4010
+ ? React.createElement('div', {
4011
+ className: 'edrv-side-diff-dock edrv-diff-dock-shell',
4012
+ 'data-edrv-diff-dock-shell': '1',
4013
+ }, React.createElement(DiffBox, { ...dockSnapshot, dock: true }))
4014
+ : null
4015
+ // 主编辑列(侧边栏右侧):pathBar + tabRow + 编辑/差异区。
3934
4016
  const editorArea = React.createElement('div', { className: 'edrv-editor-area' },
3935
4017
  dapBar,
3936
4018
  addCfgBtn,
3937
4019
  body,
3938
4020
  hoverEl,
4021
+ sideDock,
3939
4022
  overlay)
3940
4023
  // 侧边栏引导条(仅旧页签形态且未装 betterSidebar 时显示;可复制安装命令、可关闭)
3941
4024
  const dismissHint = () => {
@@ -9,6 +9,9 @@
9
9
  import React from 'react'
10
10
  import { rpc } from '../rpc.js'
11
11
  import type { MpcProject, MpcServer } from '../../shared/mcp.js'
12
+ import { configOf } from '../mcpForm.js'
13
+ import { McpState } from '../mcpState.js'
14
+ import { statusOf, statusTone } from '../../shared/mcpStatus.js'
12
15
  import '../styles/mcp.css'
13
16
  import { availableOpeners, AUTO_OPEN_TOOL } from '../fileOpeners.js'
14
17
  import { SettingsContext } from '../settingsContext.js'
@@ -24,44 +27,15 @@ import { AiSettings } from './AiSettings.js'
24
27
 
25
28
  const EMPTY = { serverName: '', transport: 'stdio', command: '', args: '', cwd: '', url: '', headers: '' }
26
29
 
27
- /** 将表单草稿转换为 Host MCP 配置。 */
28
- function configOf(draft) {
29
- const config = { serverName: draft.serverName.trim(), transport: draft.transport }
30
- if (draft.transport === 'stdio') {
31
- config.command = draft.command.trim()
32
- config.args = draft.args.split(/\r?\n|\s+/).filter(Boolean)
33
- if (draft.cwd.trim()) config.cwd = draft.cwd.trim()
34
- } else {
35
- config.url = draft.url.trim()
36
- config.headers = parsePairs(draft.headers)
37
- }
38
- return config
39
- }
40
-
41
- /** 解析每行 key=value 的表单字段。 */
42
- function parsePairs(text) {
43
- return Object.fromEntries(text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => {
44
- const idx = line.indexOf('=')
45
- return idx > 0 ? [line.slice(0, idx).trim(), line.slice(idx + 1).trim()] : [line, '']
46
- }))
47
- }
48
-
49
- /** @author ddj 2026年09月28号 @param server Server lifecycle snapshot. @returns User-facing status. */
50
- function statusOf(server) {
51
- if (!server.enabled || server.status === 'disabled') return '已禁用'
52
- const labels = { connected: '在线', connecting: '装配中', configured: '已配置,等待会话', unverified: '已加载,连接未确认', error: '错误' }
53
- return labels[server.status] ?? '状态未知'
54
- }
55
-
56
- /** MCP 服务卡片(全局与项目共用)。 */
57
- function ServerCard({ server, onRefresh, onToggle, onRemove }) {
30
+ /** @private @author ddj 2026年10月08号 @param props Server snapshot, page-wide busy state and actions. @returns Lifecycle-evidence card; enabled does not imply online. */
31
+ function ServerCard({ server, busy, onRefresh, onToggle, onRemove }) {
58
32
  return React.createElement('article', { className: 'vsm-mcp-card' },
59
33
  React.createElement('div', { className: 'vsm-mcp-card-head' },
60
- React.createElement('div', { className: 'vsm-mcp-title' }, React.createElement('span', { className: 'vsm-mcp-dot ' + (server.enabled ? server.status : 'disabled') }), server.serverName),
34
+ React.createElement('div', { className: 'vsm-mcp-title' }, React.createElement('span', { className: 'vsm-mcp-dot ' + statusTone(server) }), server.serverName),
61
35
  React.createElement('div', { className: 'vsm-mcp-actions' },
62
- React.createElement('button', { onClick: () => onRefresh(server.id), title: '刷新连接' }, '↻'),
63
- React.createElement('button', { className: 'vsm-danger', onClick: () => onRemove(server.id), title: '删除 MCP' }, '⌫'),
64
- React.createElement('button', { className: 'vsm-switch ' + (server.enabled ? 'on' : ''), onClick: () => onToggle(server), 'aria-label': server.enabled ? '禁用' : '启用' }, server.enabled ? '●' : '○'),
36
+ React.createElement('button', { disabled: !!busy, onClick: () => onRefresh(server.id), title: '重新装配会话实例' }, '↻'),
37
+ React.createElement('button', { disabled: !!busy, className: 'vsm-danger', onClick: () => onRemove(server.id), title: '删除 MCP' }, '⌫'),
38
+ React.createElement('button', { disabled: !!busy, className: 'vsm-switch ' + (server.enabled ? 'on' : ''), onClick: () => onToggle(server), title: '配置启用不代表连接在线', 'aria-label': server.enabled ? '禁用配置(不代表在线)' : '启用配置(不代表在线)', 'aria-pressed': !!server.enabled }, server.enabled ? '●' : '○'),
65
39
  ),
66
40
  ),
67
41
  React.createElement('div', { className: 'vsm-mcp-meta' }, statusOf(server), ' · ', server.toolCount, ' 个工具 · ', server.transport,
@@ -119,17 +93,17 @@ function ProjectPicker({ projects, value, onChange }) {
119
93
  )
120
94
  }
121
95
 
122
- /** 单个项目的 MCP 分组。 */
96
+ /** @private @author ddj 2026年10月08号 @param props Project view, shared busy lock and actions. @returns Project cards with actions disabled for any active mutation. */
123
97
  function ProjectGroup({ project, busy, onAdd, onRefresh, onToggle, onRemove }) {
124
98
  const head = React.createElement('div', { className: 'vsm-project-head' },
125
99
  React.createElement('div', { className: 'vsm-project-title' }, React.createElement('span', { className: 'vsm-project-icon' }, '▣'), React.createElement('div', null, React.createElement('span', { className: 'vsm-project-name' }, project.title), React.createElement('span', { className: 'vsm-project-path' }, project.workspacePath))),
126
- React.createElement('button', { className: 'vsm-primary vsm-small', onClick: () => onAdd(project) }, '+ 添加 MCP'),
100
+ React.createElement('button', { className: 'vsm-primary vsm-small', disabled: !!busy, onClick: () => onAdd(project) }, '+ 添加 MCP'),
127
101
  )
128
102
  let body
129
103
  if (project.missingDir) body = React.createElement('div', { className: 'vsm-project-empty' }, '项目目录已不存在,无法管理 MCP')
130
104
  else if (project.fileError) body = React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, project.fileError)
131
105
  else if (!project.servers.length) body = React.createElement('div', { className: 'vsm-project-empty' }, '此项目未配置 MCP')
132
- else body = project.servers.map((server) => React.createElement(ServerCard, { key: server.serverName, server, onRefresh: (id) => onRefresh(project, server, id), onToggle: () => onToggle(project, server), onRemove: (id) => onRemove(project, server, id) }))
106
+ else body = project.servers.map((server) => React.createElement(ServerCard, { key: server.serverName, server, busy, onRefresh: (id) => onRefresh(project, server, id), onToggle: () => onToggle(project, server), onRemove: (id) => onRemove(project, server, id) }))
133
107
  return React.createElement('section', { className: 'vsm-project' }, head, body)
134
108
  }
135
109
 
@@ -462,8 +436,10 @@ const MCP_TABS = [
462
436
  * @param {object} draft 新增表单草稿
463
437
  * @param {Function} edit 草稿字段变更回调
464
438
  * @param {Function} resetDraft 清空草稿回调
439
+ * @author ddj 2026年10月08号
465
440
  */
466
- function McpManagePanel({ servers, projects, busy, draft, edit, resetDraft, refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject, saveGlobal, saveProject }) {
441
+ function McpManagePanel(props) {
442
+ const { projects, busy, draft, edit, resetDraft, saveGlobal, saveProject } = props
467
443
  const [mcpTab, setMcpTab] = React.useState('mine')
468
444
  const [showForm, setShowForm] = React.useState(false)
469
445
  const [projectForm, setProjectForm] = React.useState(null)
@@ -477,26 +453,26 @@ function McpManagePanel({ servers, projects, busy, draft, edit, resetDraft, refr
477
453
  if (!projects.some((project) => project.workspacePath === selectedPath)) setSelectedPath(projects[0].workspacePath)
478
454
  }, [projects, selectedPath])
479
455
 
456
+ /** @author ddj 2026年10月08号 @description Close the successful global form. */
480
457
  const closeGlobalForm = () => setShowForm(false)
458
+ /** @author ddj 2026年10月08号 @description Close the successful project form. */
481
459
  const closeProjectForm = () => setProjectForm(null)
460
+ /** @author ddj 2026年10月08号 @param project Target project. @description Open a project draft only when idle. */
482
461
  const addProject = (project) => {
462
+ if (busy) return
483
463
  resetDraft?.()
464
+ setShowForm(false)
484
465
  setProjectForm({ workspacePath: project.workspacePath, title: project.title })
485
466
  }
467
+ /** @author ddj 2026年10月08号 @description Open a global draft only when idle. */
468
+ const addGlobal = () => {
469
+ if (busy) return
470
+ resetDraft?.()
471
+ setProjectForm(null)
472
+ setShowForm(true)
473
+ }
486
474
 
487
- const selectedProject = projects.find((project) => project.workspacePath === selectedPath)
488
- const mineBody = servers.length
489
- ? servers.map((s) => React.createElement(ServerCard, { key: s.id, server: s, onRefresh: refreshGlobal, onToggle: toggleGlobal, onRemove: removeGlobal }))
490
- : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有配置全局 MCP')
491
- const projectBody = projects.length ? React.createElement(React.Fragment, null,
492
- React.createElement(ProjectPicker, { projects, value: selectedPath, onChange: setSelectedPath }),
493
- selectedProject ? React.createElement(ProjectGroup, { project: selectedProject, busy, onAdd: addProject, onRefresh: refreshProject, onToggle: toggleProject, onRemove: removeProject }) : React.createElement('div', { className: 'vsm-mcp-empty' }, '请选择项目'),
494
- ) : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有项目')
495
-
496
- let body
497
- if (mcpTab === 'projects') body = projectBody
498
- else if (mcpTab === 'market') body = React.createElement('div', { className: 'vsm-mcp-empty' }, 'MCP 市场暂未接入')
499
- else body = mineBody
475
+ const body = manageBody(props, mcpTab, selectedPath, setSelectedPath, addProject)
500
476
 
501
477
  const form = showForm
502
478
  ? React.createElement(McpForm, { title: '添加全局 MCP', draft, busy, edit, save: () => saveGlobal(closeGlobalForm), close: closeGlobalForm })
@@ -506,108 +482,151 @@ function McpManagePanel({ servers, projects, busy, draft, edit, resetDraft, refr
506
482
  return React.createElement(React.Fragment, null,
507
483
  React.createElement('div', { className: 'vsm-mcp-subhead' },
508
484
  React.createElement('nav', { className: 'vsm-mcp-subtabs' }, MCP_TABS.map((item) => React.createElement('button', { key: item.id, className: mcpTab === item.id ? 'active' : '', onClick: () => setMcpTab(item.id) }, item.label))),
509
- React.createElement('button', { className: 'vsm-primary vsm-small', onClick: () => { resetDraft?.(); setShowForm(true) } }, '+ 添加全局 MCP'),
485
+ React.createElement('button', { className: 'vsm-primary vsm-small', disabled: !!busy, onClick: addGlobal }, '+ 添加全局 MCP'),
510
486
  ),
511
487
  body,
512
488
  form,
513
489
  )
514
490
  }
515
491
 
516
- /** MCP 管理主面板。 */
517
- export function McpSettings(props) {
518
- const openerRegistry = props?.openerRegistry ?? { list: () => [] }
519
- const compatSummary = props?.compatSummary ?? (() => [])
520
- const getShortcuts = props?.getShortcuts ?? (() => null)
521
- const [servers, setServers] = React.useState<MpcServer[]>([])
522
- const [projects, setProjects] = React.useState<MpcProject[]>([])
523
- const [loading, setLoading] = React.useState(true)
524
- const [busy, setBusy] = React.useState('')
525
- const [error, setError] = React.useState('')
526
- const [tab, setTab] = React.useState('general')
527
- const [draft, setDraft] = React.useState(EMPTY)
528
-
529
- const loadAll = React.useCallback(() => {
530
- setLoading(true)
531
- setError('')
532
- Promise.all([rpc('mcp.list', {}), rpc('mcp.projects', {})]).then(([list, prj]) => {
533
- if (!list.ok) throw new Error(list.error)
534
- if (!prj.ok) throw new Error(prj.error)
535
- setServers(list.servers)
536
- setProjects(prj.projects)
537
- }).catch((e) => setError(String(e))).finally(() => setLoading(false))
538
- }, [])
539
-
540
- React.useEffect(loadAll, [loadAll])
541
-
542
- const finish = (result) => {
543
- if (!result.ok) throw new Error(result.error)
544
- return result
492
+ /** @private @author ddj 2026年10月08号 @param props Current view and actions. @param tab Selected MCP subtab. @param path Selected project. @param select Project setter. @param add Project form opener. @returns Selected global/project/market content. */
493
+ function manageBody(props, tab, path, select, add) {
494
+ const { servers, projects, busy, refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject } = props
495
+ if (tab === 'market') return React.createElement('div', { className: 'vsm-mcp-empty' }, 'MCP 市场暂未接入')
496
+ if (tab !== 'projects') {
497
+ return servers.length
498
+ ? servers.map((server) => React.createElement(ServerCard, { key: server.id, server, busy, onRefresh: refreshGlobal, onToggle: toggleGlobal, onRemove: removeGlobal }))
499
+ : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有配置全局 MCP')
545
500
  }
501
+ if (!projects.length) return React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有项目')
502
+ const project = projects.find((item) => item.workspacePath === path)
503
+ return React.createElement(React.Fragment, null,
504
+ React.createElement(ProjectPicker, { projects, value: path, onChange: select }),
505
+ project ? React.createElement(ProjectGroup, { project, busy, onAdd: add, onRefresh: refreshProject, onToggle: toggleProject, onRemove: removeProject }) : React.createElement('div', { className: 'vsm-mcp-empty' }, '请选择项目'),
506
+ )
507
+ }
546
508
 
547
- const action = (label, id, method, args, patch) => {
548
- setBusy(id)
549
- setError('')
550
- return rpc(method, args).then(finish).then((result) => {
551
- if (patch) patch(result)
552
- }).catch((e) => setError(String(e))).finally(() => setBusy('')).then(() => label)
509
+ //region MCP page state and locked actions
510
+ /** @private @author ddj 2026年10月08号 @returns Mounted controller reference and its published view. */
511
+ function useMcpState() {
512
+ const ref = React.useRef(null)
513
+ const [view, setView] = React.useState({ servers: [], projects: [], loading: true, busy: '', error: '' })
514
+ /** @author ddj 2026年10月08号 @description Subscribe to document visibility and initialize MCP data. @returns Unmount cleanup. */
515
+ const mount = () => {
516
+ const state = new McpState(rpc, setView)
517
+ ref.current = state
518
+ /** @author ddj 2026年10月08号 @description Gate snapshots on actual document visibility. */
519
+ const visibility = () => state.setVisible(document.visibilityState === 'visible')
520
+ /** @author ddj 2026年10月08号 @description Stop controller and remove page listener. */
521
+ const cleanup = () => {
522
+ document.removeEventListener('visibilitychange', visibility)
523
+ state.dispose()
524
+ if (ref.current === state) ref.current = null
525
+ }
526
+ visibility()
527
+ document.addEventListener('visibilitychange', visibility)
528
+ void state.load()
529
+ return cleanup
553
530
  }
531
+ React.useEffect(mount, [])
532
+ return { ref, view }
533
+ }
554
534
 
555
- const refreshGlobal = (id) => action('refresh', id, 'mcp.refresh', { id }, (r) => setServers((old) => old.map((s) => s.id === id ? r.server : s)))
556
- const toggleGlobal = (s) => action('toggle', s.id, 'mcp.toggle', { id: s.id, enabled: !s.enabled }, (r) => setServers((old) => old.map((x) => x.id === s.id ? r.server : x)))
557
- const removeGlobal = (s) => window.confirm('确认删除此 MCP 服务?') && action('remove', s.id, 'mcp.remove', { id: s.id }, (r) => setServers((old) => old.filter((x) => x.id !== s.id)))
558
-
559
- const replaceProject = (workspacePath) => (result) => setProjects((old) => old.map((p) => p.workspacePath === workspacePath ? result.project : p))
560
- const projectAction = (workspacePath, serverName, method, args, patch) => {
561
- setBusy(workspacePath + ':' + serverName)
562
- setError('')
563
- return rpc(method, args).then(finish).then((result) => {
564
- if (patch) patch(result)
565
- }).catch((e) => setError(String(e))).finally(() => setBusy(''))
535
+ /** @private @author ddj 2026年10月08号 @param state Mounted controller. @param id Server identity. @param method Mutation RPC. @param args Request fields. @returns Locked completion. */
536
+ function globalAction(state, id, method, args) {
537
+ /** @author ddj 2026年10月08号 @returns Global mutation result. */
538
+ const request = () => rpc(method, args)
539
+ /** @author ddj 2026年10月08号 @param result Global mutation result. @description Apply only successful mutation to the latest controller view. */
540
+ const commit = (result) => {
541
+ state.view.servers = method === 'mcp.remove'
542
+ ? state.view.servers.filter((server) => server.id !== id)
543
+ : state.view.servers.map((server) => server.id === id ? result.server : server)
566
544
  }
545
+ return state?.mutate(id, request, commit)
546
+ }
567
547
 
568
- const refreshProject = (p, s) => projectAction(p.workspacePath, s.serverName, 'mcp.projectRefresh', { workspacePath: p.workspacePath, serverName: s.serverName }, replaceProject(p.workspacePath))
569
- const toggleProject = (p, s) => projectAction(p.workspacePath, s.serverName, 'mcp.projectToggle', { workspacePath: p.workspacePath, serverName: s.serverName, enabled: !s.enabled }, replaceProject(p.workspacePath))
570
- const removeProject = (p, s) => window.confirm('确认删除此项目的 MCP「' + s.serverName + '」?') && projectAction(p.workspacePath, s.serverName, 'mcp.projectRemove', { workspacePath: p.workspacePath, serverName: s.serverName }, replaceProject(p.workspacePath))
571
-
572
- /**
573
- * 保存全局 MCP:成功后由回调关闭子面板。
574
- * 作者 ddj 2026年08月27号
575
- * @param {Function} close 关闭新增表单回调
576
- */
577
- const saveGlobal = (close) => {
578
- setBusy('save')
579
- setError('')
580
- rpc('mcp.save', { config: configOf(draft) }).then(finish).then((result) => {
581
- setServers((old) => old.some((s) => s.id === result.server.id) ? old.map((s) => s.id === result.server.id ? result.server : s) : old.concat(result.server))
582
- setDraft(EMPTY)
583
- close()
584
- }).catch((e) => setError(String(e))).finally(() => setBusy(''))
548
+ /** @private @author ddj 2026年10月08号 @param state Mounted controller. @param path Project path. @param name Server identity. @param method Mutation RPC. @param extra Optional request fields. @returns Locked completion. */
549
+ function projectAction(state, path, name, method, extra = {}) {
550
+ /** @author ddj 2026年10月08号 @returns Project mutation result. */
551
+ const request = () => rpc(method, { workspacePath: path, serverName: name, ...extra })
552
+ /** @author ddj 2026年10月08号 @param result Project mutation result. @description Replace project in the current view. */
553
+ const commit = (result) => {
554
+ state.view.projects = state.view.projects.map((project) => project.workspacePath === path ? result.project : project)
585
555
  }
556
+ return state?.mutate(path + ':' + name, request, commit)
557
+ }
586
558
 
587
- /**
588
- * 保存项目 MCP:成功后由回调关闭子面板。
589
- * 作者 ddj 2026年08月27号
590
- * @param {string} workspacePath 目标项目路径
591
- * @param {Function} close 关闭新增表单回调
592
- */
593
- const saveProject = (workspacePath, close) => {
594
- setBusy('project-save')
595
- setError('')
559
+ /** @private @author ddj 2026年10月08号 @param ref Mounted controller reference. @returns Global/project card actions sharing a synchronous lock. */
560
+ function cardActions(ref) {
561
+ /** @author ddj 2026年10月08号 @param id Global identity. @returns Locked refresh. */
562
+ const refreshGlobal = (id) => globalAction(ref.current, id, 'mcp.refresh', { id })
563
+ /** @author ddj 2026年10月08号 @param server Current configuration. @returns Locked enable change. */
564
+ const toggleGlobal = (server) => globalAction(ref.current, server.id, 'mcp.toggle', { id: server.id, enabled: !server.enabled })
565
+ /** @author ddj 2026年10月08号 @param id Global identity from ServerCard. @returns Confirmed locked deletion. */
566
+ const removeGlobal = (id) => !ref.current?.view.busy && window.confirm('确认删除此 MCP 服务?') && globalAction(ref.current, id, 'mcp.remove', { id })
567
+ /** @author ddj 2026年10月08号 @param project Project. @param server Configuration. @returns Locked refresh. */
568
+ const refreshProject = (project, server) => projectAction(ref.current, project.workspacePath, server.serverName, 'mcp.projectRefresh')
569
+ /** @author ddj 2026年10月08号 @param project Project. @param server Configuration. @returns Locked enable change. */
570
+ const toggleProject = (project, server) => projectAction(ref.current, project.workspacePath, server.serverName, 'mcp.projectToggle', { enabled: !server.enabled })
571
+ /** @author ddj 2026年10月08号 @param project Project. @param server Configuration. @returns Confirmed locked deletion. */
572
+ const removeProject = (project, server) => !ref.current?.view.busy && window.confirm('确认删除此项目的 MCP「' + server.serverName + '」?') && projectAction(ref.current, project.workspacePath, server.serverName, 'mcp.projectRemove')
573
+ return { refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject }
574
+ }
575
+
576
+ /** @private @author ddj 2026年10月08号 @param state Controller. @param draft Editable fields. @param setDraft Draft setter. @param path Optional project target. @param close Success-only close callback. @returns Locked save, validation included. */
577
+ function saveDraft(state, draft, setDraft, path, close) {
578
+ /** @author ddj 2026年10月08号 @returns Validated save result. @throws Malformed arguments/headers. */
579
+ const request = () => {
596
580
  const config = configOf(draft)
597
- rpc('mcp.projectSave', { workspacePath, serverName: config.serverName, config }).then(finish).then((result) => {
598
- setProjects((old) => old.map((p) => p.workspacePath === workspacePath ? result.project : p))
599
- setDraft(EMPTY)
600
- close()
601
- }).catch((e) => setError(String(e))).finally(() => setBusy(''))
581
+ return path === null ? rpc('mcp.save', { config }) : rpc('mcp.projectSave', { workspacePath: path, serverName: config.serverName, config })
602
582
  }
583
+ /** @author ddj 2026年10月08号 @param result Save result. @description Update current view and only then reset/close the form. */
584
+ const commit = (result) => {
585
+ if (path !== null) state.view.projects = state.view.projects.map((project) => project.workspacePath === path ? result.project : project)
586
+ else {
587
+ const servers = state.view.servers
588
+ state.view.servers = servers.some((server) => server.id === result.server.id)
589
+ ? servers.map((server) => server.id === result.server.id ? result.server : server) : servers.concat(result.server)
590
+ }
591
+ setDraft(EMPTY)
592
+ close()
593
+ }
594
+ return state?.mutate(path === null ? 'save' : 'project-save', request, commit)
595
+ }
596
+ //endregion
603
597
 
604
- const edit = (key, value) => setDraft((old) => ({ ...old, [key]: value }))
605
- const resetDraft = () => setDraft(EMPTY)
598
+ /** @public @author ddj 2026年10月08号 @param props Settings integrations. @returns MCP management page with visible-tab-only snapshot polling. */
599
+ export function McpSettings(props) {
600
+ const openerRegistry = props?.openerRegistry ?? { list: () => [] }
601
+ const compatSummary = props?.compatSummary ?? (() => [])
602
+ const getShortcuts = props?.getShortcuts ?? (() => null)
603
+ const { ref, view } = useMcpState()
604
+ const { servers, projects, loading, busy, error } = view
605
+ const [tab, setTab] = React.useState('general')
606
+ const [draft, setDraft] = React.useState(EMPTY)
607
+ /** @author ddj 2026年10月08号 @description Select polling scope and invalidate late responses on tab leave. @returns Tab cleanup. */
608
+ const selectTab = () => {
609
+ const state = ref.current
610
+ state?.setActive(tab === 'mcp')
611
+ /** @author ddj 2026年10月08号 @description Cancel reads on tab switch. */
612
+ const cleanup = () => state?.setActive(false)
613
+ return cleanup
614
+ }
615
+ React.useEffect(selectTab, [tab])
616
+ const { refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject } = cardActions(ref)
617
+ /** @author ddj 2026年10月08号 @param close Success-only close callback. @returns Global save completion. */
618
+ const saveGlobal = (close) => saveDraft(ref.current, draft, setDraft, null, close)
619
+ /** @author ddj 2026年10月08号 @param path Target project. @param close Success-only close callback. @returns Project save completion. */
620
+ const saveProject = (path, close) => saveDraft(ref.current, draft, setDraft, path, close)
621
+ /** @author ddj 2026年10月08号 @param key Draft field. @param value User input. @description Keep draft edits unavailable during a save. */
622
+ const edit = (key, value) => { if (!ref.current?.view.busy) setDraft((old) => ({ ...old, [key]: value })) }
623
+ /** @author ddj 2026年10月08号 @description Reset the draft when no mutation is pending. */
624
+ const resetDraft = () => { if (!ref.current?.view.busy) setDraft(EMPTY) }
606
625
  let body
607
626
  if (loading) body = React.createElement('div', { className: 'vsm-mcp-empty' }, '正在读取 MCP 服务…')
608
627
  else if (tab === 'general') body = React.createElement(GeneralSettings, { registry: openerRegistry, getShortcuts })
609
628
  else if (tab === 'mcp') body = React.createElement(McpManagePanel, { servers, projects, busy, draft, edit, resetDraft,
610
- saveGlobal: (close) => saveGlobal(close), saveProject: (workspacePath, close) => saveProject(workspacePath, close),
629
+ saveGlobal, saveProject,
611
630
  refreshGlobal, toggleGlobal, removeGlobal, refreshProject, toggleProject, removeProject })
612
631
  else if (tab === 'compat') body = React.createElement(CompatSection, { getSummary: compatSummary })
613
632
  else if (tab === 'lsp') body = React.createElement(LspSettings, null)
@@ -621,9 +640,27 @@ export function McpSettings(props) {
621
640
  )
622
641
  }
623
642
 
624
- /** MCP 新增表单(全局/项目共用)。 */
643
+ /** @private @author ddj 2026年10月08号 @param props Draft, shared busy lock, edit/save/close callbacks. @returns Lossless argument/header form; failed save stays open. */
625
644
  function McpForm({ title, draft, busy, edit, save, close }) {
626
- const input = (key, placeholder) => React.createElement('input', { value: draft[key], onChange: (e) => edit(key, e.target.value), placeholder })
627
- const fields = draft.transport === 'stdio' ? React.createElement(React.Fragment, null, React.createElement('label', null, '命令', input('command', 'npx')), React.createElement('label', null, '参数(空格或换行分隔)', React.createElement('textarea', { value: draft.args, onChange: (e) => edit('args', e.target.value) })), React.createElement('label', null, '工作目录(可选)', input('cwd', ''))) : React.createElement(React.Fragment, null, React.createElement('label', null, 'URL', input('url', 'http://localhost:3000/mcp')), React.createElement('label', null, '请求头(每行 key=value)', React.createElement('textarea', { value: draft.headers, onChange: (e) => edit('headers', e.target.value) })))
628
- return React.createElement('div', { className: 'vsm-mcp-modal' }, React.createElement('div', { className: 'vsm-mcp-dialog' }, React.createElement('h3', null, title), React.createElement('label', null, '名称', input('serverName', '例如 codegraph')), React.createElement('label', null, '传输方式', React.createElement('select', { value: draft.transport, onChange: (e) => edit('transport', e.target.value) }, React.createElement('option', { value: 'stdio' }, 'stdio'), React.createElement('option', { value: 'streamable-http' }, 'streamable-http'))), fields, React.createElement('div', { className: 'vsm-mcp-dialog-actions' }, React.createElement('button', { onClick: close }, '取消'), React.createElement('button', { className: 'vsm-primary', disabled: busy === 'save' || busy === 'project-save', onClick: save }, '保存并连接'))))
645
+ /** @author ddj 2026年10月08号 @param key Draft field. @returns Short input change callback. */
646
+ const change = (key) => (event) => edit(key, event.target.value)
647
+ /** @author ddj 2026年10月08号 @param key Draft field. @param placeholder Input hint. @returns Disabled-while-busy input. */
648
+ const input = (key, placeholder) => React.createElement('input', { disabled: !!busy, value: draft[key], onChange: change(key), placeholder })
649
+ /** @author ddj 2026年10月08号 @param key Draft field. @returns Disabled-while-busy text area. */
650
+ const textarea = (key) => React.createElement('textarea', { disabled: !!busy, value: draft[key], onChange: change(key) })
651
+ const fields = draft.transport === 'stdio' ? React.createElement(React.Fragment, null,
652
+ React.createElement('label', null, '命令', input('command', 'npx')),
653
+ React.createElement('label', null, '参数(JSON string[] 或每行一个;保留空格,空参数用 JSON)', textarea('args')),
654
+ React.createElement('label', null, '工作目录(可选)', input('cwd', '')),
655
+ ) : React.createElement(React.Fragment, null,
656
+ React.createElement('label', null, 'URL', input('url', 'http://localhost:3000/mcp')),
657
+ React.createElement('label', null, '请求头(每行 key=value,键不可重复)', textarea('headers')),
658
+ )
659
+ return React.createElement('div', { className: 'vsm-mcp-modal' }, React.createElement('div', { className: 'vsm-mcp-dialog' },
660
+ React.createElement('h3', null, title),
661
+ React.createElement('label', null, '名称', input('serverName', '例如 codegraph')),
662
+ React.createElement('label', null, '传输方式', React.createElement('select', { disabled: !!busy, value: draft.transport, onChange: change('transport') }, React.createElement('option', { value: 'stdio' }, 'stdio'), React.createElement('option', { value: 'streamable-http' }, 'streamable-http'))),
663
+ fields,
664
+ React.createElement('div', { className: 'vsm-mcp-dialog-actions' }, React.createElement('button', { disabled: !!busy, onClick: close }, '取消'), React.createElement('button', { className: 'vsm-primary', disabled: !!busy, onClick: save }, '保存配置')),
665
+ ))
629
666
  }
package/src/dshVersion.ts CHANGED
@@ -84,16 +84,17 @@ export function inDshRange(version: DshVersion | null | undefined, range: DshRan
84
84
 
85
85
  /**
86
86
  * 版本线标签:报告与文档展示版本归属;不可解析返回 '未知'。
87
- * 逐线自新到旧匹配,先命中先返回;仅 0.2.0-rc.1 精确标记为已审计版本。
88
- * @author ddj 2026年09月02号 / 2026年09月18号 / 2026年09月22号 / 2026年09月29号
87
+ * 逐线自新到旧匹配,先命中先返回;仅已静态审计的 0.2.0-rc.1/rc.2 精确标记。
88
+ * @author ddj 2026年09月02号 / 2026年09月18号 / 2026年09月22号 / 2026年09月29号 / 2026年10月08号
89
89
  * @param input 版本串(如 '0.1.6-alpha.2')
90
90
  * @returns 版本线标签
91
91
  */
92
92
  export function familyLabel(input: string): string {
93
93
  const version = parseDshVersion(input)
94
94
  if (!version) return '未知'
95
+ if (inDshRange(version, { from: '0.2.0-rc.2', to: '0.2.0-rc.2' })) return '0.2.0-rc.2(设置=profile Config;按能力探测)'
95
96
  if (inDshRange(version, { from: '0.2.0-rc.1', to: '0.2.0-rc.1' })) return '0.2.0-rc.1(设置=profile Config;按能力探测)'
96
- if (inDshRange(version, { from: '0.2.0-0' })) return '0.2.0 及更新(除 rc.1 外未经逐版适配;按能力探测)'
97
+ if (inDshRange(version, { from: '0.2.0-0' })) return '0.2.0 及更新(除 rc.1/rc.2 外未经逐版适配;按能力探测)'
97
98
  if (inDshRange(version, { from: '0.1.7-alpha.1' })) return '0.1.7-alpha.1 及更新(设置=profile Config + configForms 客户端面 + 会话 V4)'
98
99
  if (inDshRange(version, { from: '0.1.6-alpha.2' })) return '0.1.6-alpha.2 及更新(会话多实例共存 + 回合改动卡片 + Office 侧栏预览 + 侧栏浏览器)'
99
100
  if (inDshRange(version, { from: '0.1.6-alpha.1' })) return '0.1.6-alpha 及更新(MCP SDK v2 + Web 侧边栏终端 + 文件链接默认侧栏预览)'