dsh-vscode-mode 0.14.3 → 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.
@@ -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 侧边栏终端 + 文件链接默认侧栏预览)'