dsh-vscode-mode 0.14.3 → 0.14.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -9
- package/lib/client.js +1445 -400
- package/lib/client.js.map +1 -1
- package/lib/index.js +242 -53
- package/lib/index.js.map +1 -1
- package/package.json +7 -7
- package/skills/dsh-vscodemode-mcp/SKILL.md +9 -3
- package/src/client/diffDockStore.ts +8 -3
- package/src/client/index.ts +2 -0
- package/src/client/mcpForm.ts +79 -0
- package/src/client/mcpState.ts +179 -0
- package/src/client/monaco/diffRender.ts +234 -174
- package/src/client/monaco/theme.ts +26 -15
- package/src/client/officialSidebar.ts +6 -1
- package/src/client/openReceipt.ts +13 -3
- package/src/client/planBridge.ts +178 -0
- package/src/client/planState.ts +129 -0
- package/src/client/receiptState.ts +1 -0
- package/src/client/state/regions.ts +83 -3
- package/src/client/styles/editor.css +8 -7
- package/src/client/styles/mcp.css +6 -3
- package/src/client/ui/ClaimRouter.ts +1 -1
- package/src/client/ui/EditorView.ts +107 -36
- package/src/client/ui/McpSettings.ts +174 -137
- package/src/client/ui/OfficialSideTab.ts +2 -2
- package/src/dshVersion.ts +4 -3
- package/src/mcp.ts +131 -23
- package/src/mcpIsolation.ts +2 -1
- package/src/mcpProject.ts +74 -14
- package/src/mcpRuntime.ts +18 -3
- package/src/rpc.ts +2 -1
- package/src/shared/mcpStatus.ts +30 -0
- package/src/shared/rpc.ts +2 -0
|
@@ -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
|
-
/**
|
|
28
|
-
function
|
|
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
|
|
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 ? '
|
|
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
|
-
/**
|
|
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(
|
|
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
|
|
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',
|
|
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
|
-
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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 })
|
|
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
|
-
|
|
605
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
627
|
-
const
|
|
628
|
-
|
|
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
|
}
|
|
@@ -69,14 +69,14 @@ export function OfficialSideTab(props) {
|
|
|
69
69
|
if (fileHit) {
|
|
70
70
|
const line = navigation?.params?.line
|
|
71
71
|
window.dispatchEvent(new CustomEvent('edrv:open-editor', {
|
|
72
|
-
detail: { path: fileHit.path, line: typeof line === 'number' ? line : undefined },
|
|
72
|
+
detail: { path: fileHit.path, line: typeof line === 'number' ? line : undefined, preview: navigation?.params?.preview },
|
|
73
73
|
}))
|
|
74
74
|
return
|
|
75
75
|
}
|
|
76
76
|
// 页类型 Tab:params 携带打开请求(line = 行引用/工具行跳转的行号定位)
|
|
77
77
|
const request = resolveNavOpen(navigation?.params)
|
|
78
78
|
window.dispatchEvent(new CustomEvent('edrv:open-editor', {
|
|
79
|
-
detail: { path: request.path, focusDiff: request.focusDiff, line: resolveNavLine(navigation?.params) },
|
|
79
|
+
detail: { path: request.path, focusDiff: request.focusDiff, line: resolveNavLine(navigation?.params), preview: navigation?.params?.preview },
|
|
80
80
|
}))
|
|
81
81
|
})
|
|
82
82
|
|
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
|
-
*
|
|
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 侧边栏终端 + 文件链接默认侧栏预览)'
|