dsh-vscode-mode 0.1.10 → 0.1.12
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 +12 -0
- package/lib/client.js +537 -54
- package/lib/client.js.map +1 -1
- package/lib/index.js +1003 -94
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/capture.ts +44 -8
- package/src/client/index.ts +11 -2
- package/src/client/monaco/diffRender.ts +1 -1
- package/src/client/state/records.ts +5 -2
- package/src/client/state/regions.ts +26 -20
- package/src/client/styles/mcp.css +87 -0
- package/src/client/ui/DiffBox.ts +1 -0
- package/src/client/ui/McpSettings.ts +233 -0
- package/src/index.ts +4 -2
- package/src/mcp.ts +123 -0
- package/src/mcpIsolation.ts +160 -0
- package/src/mcpProject.ts +258 -0
- package/src/model.ts +66 -34
- package/src/revert.ts +10 -3
- package/src/rpc.ts +44 -0
- package/src/shared/diff.ts +201 -0
- package/src/shared/mcp.ts +53 -0
- package/src/shared/rpc.ts +21 -0
- package/src/shared/types.ts +15 -0
- package/src/store.ts +29 -31
package/src/capture.ts
CHANGED
|
@@ -3,13 +3,41 @@
|
|
|
3
3
|
* 迁移自原 src/index.ts 的 tools/result 处理,语义不改。
|
|
4
4
|
* 作者 ddj 2026-08-20
|
|
5
5
|
*/
|
|
6
|
-
import type { DiffRecord } from './shared/types.js'
|
|
6
|
+
import type { DiffRecord, Hunk } from './shared/types.js'
|
|
7
|
+
import { annotateHunks, fingerprint } from './shared/diff.js'
|
|
7
8
|
import type { Ctx, Session } from './store.js'
|
|
8
9
|
import { archiveRecords, loadBucket, saveBucket } from './store.js'
|
|
9
10
|
import { fileMaxBatch, prune, recordResolved } from './model.js'
|
|
10
11
|
import type { Registry } from './registry.js'
|
|
11
12
|
import { cwdOf } from './registry.js'
|
|
12
13
|
|
|
14
|
+
/** 校验并提取工具 result meta 中的文件 hunk。 */
|
|
15
|
+
function validHunks(raw: unknown): Hunk[] {
|
|
16
|
+
if (!Array.isArray(raw)) return []
|
|
17
|
+
const hunks: Hunk[] = []
|
|
18
|
+
for (const item of raw) {
|
|
19
|
+
if (!item || typeof item !== 'object') continue
|
|
20
|
+
const hunk = item as Record<string, unknown>
|
|
21
|
+
if (typeof hunk.newText !== 'string') continue
|
|
22
|
+
if (hunk.oldText !== null && typeof hunk.oldText !== 'string') continue
|
|
23
|
+
hunks.push({ oldText: hunk.oldText as string | null, newText: hunk.newText })
|
|
24
|
+
}
|
|
25
|
+
return hunks
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 已有文件 write 的元数据缺失时,用权威整文件快照保底。 */
|
|
29
|
+
function fallbackHunks(toolName: string, args: Record<string, unknown>, before: string | null, after: string | null, create: boolean): Hunk[] {
|
|
30
|
+
if (create) return [{ oldText: null, newText: after ?? (typeof args.content === 'string' ? args.content : '') }]
|
|
31
|
+
if (toolName === 'edit' && typeof args.old_string === 'string' && typeof args.new_string === 'string') {
|
|
32
|
+
return [{ oldText: args.old_string, newText: args.new_string }]
|
|
33
|
+
}
|
|
34
|
+
if (toolName === 'write' && before !== null && after !== null && before !== after) {
|
|
35
|
+
return [{ oldText: before, newText: after }]
|
|
36
|
+
}
|
|
37
|
+
if (toolName === 'write' && after !== null) return [{ oldText: null, newText: after }]
|
|
38
|
+
return []
|
|
39
|
+
}
|
|
40
|
+
|
|
13
41
|
/**
|
|
14
42
|
* 捕获一次工具执行结果(tools/result):edit/write 成功时构造记录、
|
|
15
43
|
* 递增文件批次、融合归档被后续修改取代的旧差异、落盘。
|
|
@@ -28,21 +56,29 @@ export async function captureToolResult(ctx: Ctx, registry: Registry, exec: any,
|
|
|
28
56
|
const cwd = cwdOf(session)
|
|
29
57
|
if (!cwd) return
|
|
30
58
|
const value = result.value
|
|
31
|
-
const args = exec.arguments || {}
|
|
59
|
+
const args = (exec.arguments || {}) as Record<string, unknown>
|
|
32
60
|
const path = typeof value.path === 'string' ? value.path : args.file_path
|
|
33
|
-
if (!path) return
|
|
34
|
-
const metaDiffs: DiffRecord['hunks'] = Array.isArray(result.meta?.diffs) ? result.meta.diffs : []
|
|
61
|
+
if (typeof path !== 'string' || !path) return
|
|
35
62
|
const before = typeof value.before === 'string' ? value.before : null
|
|
63
|
+
const after = typeof value.after === 'string' ? value.after : (typeof args.content === 'string' ? args.content : null)
|
|
36
64
|
const create = exec.name === 'write' && value.before === null
|
|
37
|
-
const callHunk = exec.name === 'edit' && typeof args.old_string === 'string'
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
65
|
+
const callHunk = exec.name === 'edit' && typeof args.old_string === 'string' && typeof args.new_string === 'string'
|
|
66
|
+
? { oldText: args.old_string, newText: args.new_string }
|
|
67
|
+
: null
|
|
68
|
+
const metaHunks = validHunks(result.meta?.diffs)
|
|
69
|
+
const rawHunks = metaHunks.length ? metaHunks : fallbackHunks(exec.name, args, before, after, create)
|
|
70
|
+
const hunks = annotateHunks(rawHunks, before, after)
|
|
71
|
+
if (!hunks.length || typeof exec.callId !== 'string') return
|
|
41
72
|
const record: DiffRecord = {
|
|
42
73
|
callId: exec.callId,
|
|
43
74
|
toolName: exec.name,
|
|
44
75
|
path,
|
|
45
76
|
before,
|
|
77
|
+
after,
|
|
78
|
+
baseFingerprint: fingerprint(before),
|
|
79
|
+
afterFingerprint: fingerprint(after),
|
|
80
|
+
legacy: false,
|
|
81
|
+
conflict: false,
|
|
46
82
|
create,
|
|
47
83
|
callHunk,
|
|
48
84
|
hunks,
|
package/src/client/index.ts
CHANGED
|
@@ -15,8 +15,9 @@ import React from 'react'
|
|
|
15
15
|
import './styles/editor.css'
|
|
16
16
|
import { EditorView } from './ui/EditorView.js'
|
|
17
17
|
import { DiffBadge } from './ui/DiffBadge.js'
|
|
18
|
+
import { McpSettings } from './ui/McpSettings.js'
|
|
18
19
|
|
|
19
|
-
export const inject = ['slots', 'timer']
|
|
20
|
+
export const inject = ['slots', 'timer', 'locale', 'connection', 'remote']
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* 装配客户端:注册中央编辑区视图与 header 差异角标。
|
|
@@ -41,4 +42,12 @@ export function apply(ctx: any): void {
|
|
|
41
42
|
{ name: 'conversation.session.header.utilities', id: 'edrv-diff-badge', order: 90, label: '差异' },
|
|
42
43
|
(props: unknown) => React.createElement(DiffBadge, Object.assign({}, props)),
|
|
43
44
|
))
|
|
44
|
-
|
|
45
|
+
|
|
46
|
+
// 设置页中的 VSCodeMode 专属页签,MCP 管理作为该页签的内部子 Tab。
|
|
47
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
48
|
+
name: 'settings.section',
|
|
49
|
+
id: 'vscode-mode',
|
|
50
|
+
order: 30,
|
|
51
|
+
label: 'VSCodeMode',
|
|
52
|
+
}, () => React.createElement(McpSettings)))
|
|
53
|
+
}
|
|
@@ -28,7 +28,7 @@ export function createDiffRenderer(log) {
|
|
|
28
28
|
const decos = []
|
|
29
29
|
for (const r of pendingRegions) {
|
|
30
30
|
if (r.start === undefined || r.end === undefined) continue
|
|
31
|
-
if (
|
|
31
|
+
if (r.newLines && r.newLines.length) {
|
|
32
32
|
decos.push({
|
|
33
33
|
range: new monaco.Range(r.start, 1, Math.max(r.start, r.end - 1), 1),
|
|
34
34
|
options: { isWholeLine: true, className: 'edrv-mn-add-line', linesDecorationsClassName: 'edrv-mn-gutter-add' },
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* 作者 ddj 2026-08-20
|
|
5
5
|
*/
|
|
6
6
|
import type { CallHunk, Hunk, RecordView } from '../../shared/types.js'
|
|
7
|
+
import { preciseHunk } from '../../shared/diff.js'
|
|
7
8
|
|
|
8
9
|
/** 差异决策状态。 */
|
|
9
10
|
export const ST = { PENDING: 'pending', ACCEPTED: 'accepted', REJECTED: 'rejected' } as const
|
|
@@ -36,7 +37,9 @@ export function statusAt(rec: RecordView | undefined, idx: number): Status {
|
|
|
36
37
|
/** hunk 是否为空差异(old===new,无实际内容变化,不可操作)。create 记录不视为空差异。 */
|
|
37
38
|
export function noopHunk(rec: RecordView | undefined, h: Hunk | CallHunk | null | undefined): boolean {
|
|
38
39
|
if (!rec || rec.create === true) return false
|
|
39
|
-
const precise = rec
|
|
40
|
+
const precise = rec && h && rec.hunks.indexOf(h as Hunk) >= 0
|
|
41
|
+
? preciseHunk(rec, rec.hunks.indexOf(h as Hunk))
|
|
42
|
+
: h
|
|
40
43
|
const oldText = precise?.oldText ?? null
|
|
41
44
|
const newText = precise?.newText ?? null
|
|
42
45
|
return oldText !== null && oldText === newText
|
|
@@ -44,7 +47,7 @@ export function noopHunk(rec: RecordView | undefined, h: Hunk | CallHunk | null
|
|
|
44
47
|
|
|
45
48
|
/** 记录是否仍有待处理差异(superseded / 全部已决策 = false)。 */
|
|
46
49
|
export function isRecPending(rec: RecordView): boolean {
|
|
47
|
-
if (!rec || rec.superseded === true) return false
|
|
50
|
+
if (!rec || rec.superseded === true || rec.conflict === true) return false
|
|
48
51
|
const perHunk = Array.isArray(rec.decisions?.perHunk) ? rec.decisions.perHunk : []
|
|
49
52
|
if (perHunk.length) {
|
|
50
53
|
// 只认存在至少一个"非空差异且未决策"的 hunk
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* 作者 ddj 2026-08-20
|
|
5
5
|
*/
|
|
6
6
|
import type { Hunk, RecordView } from '../../shared/types.js'
|
|
7
|
+
import { locateHunks, lineBefore, preciseHunk, splitLines } from '../../shared/diff.js'
|
|
7
8
|
import { ST, noopHunk, statusAt } from './records.js'
|
|
8
9
|
import type { Status } from './records.js'
|
|
9
10
|
|
|
@@ -25,9 +26,7 @@ export interface Region {
|
|
|
25
26
|
|
|
26
27
|
/** 统计 index 之前(不含)的换行数 → 0 基行号。 */
|
|
27
28
|
export function countLinesBefore(text: string, index: number): number {
|
|
28
|
-
|
|
29
|
-
for (let i = 0; i < index; i++) if (text.charCodeAt(i) === 10) n++
|
|
30
|
-
return n
|
|
29
|
+
return lineBefore(text, index)
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
/**
|
|
@@ -51,29 +50,36 @@ export function trimCommonLines(oldLines: string[], newLines: string[]): { oldLi
|
|
|
51
50
|
export function diffRegions(records: RecordView[], content: string | null): Region[] {
|
|
52
51
|
const regions: Region[] = []
|
|
53
52
|
if (content === null) return regions
|
|
54
|
-
const lines = content
|
|
53
|
+
const lines = splitLines(content)
|
|
55
54
|
for (const rec of records) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
regions.push({ callId: rec.callId, idx: i, start: 1, end: lines.length, oldLines: [], newLines: lines.slice(), whole: true, status, create: true, rec, superseded: rec.superseded === true })
|
|
62
|
-
continue
|
|
55
|
+
if (rec.create) {
|
|
56
|
+
for (let i = 0; i < rec.hunks.length; i++) {
|
|
57
|
+
const h = preciseHunk(rec, i)
|
|
58
|
+
if (!h || noopHunk(rec, h)) continue
|
|
59
|
+
regions.push({ callId: rec.callId, idx: i, start: 1, end: lines.length + 1, oldLines: [], newLines: lines.slice(), whole: true, status: statusAt(rec, i), create: true, rec, superseded: rec.superseded === true })
|
|
63
60
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
const entries: Array<{ idx: number; hunk: Hunk }> = []
|
|
64
|
+
for (let i = 0; i < rec.hunks.length; i++) {
|
|
65
|
+
const hunk = preciseHunk(rec, i)
|
|
66
|
+
if (hunk && !noopHunk(rec, hunk)) entries.push({ idx: i, hunk })
|
|
67
|
+
}
|
|
68
|
+
const locations = locateHunks(content, entries.map((entry) => entry.hunk))
|
|
69
|
+
for (let i = 0; i < entries.length; i++) {
|
|
70
|
+
const entry = entries[i]
|
|
71
|
+
const location = locations[i]
|
|
72
|
+
const status = statusAt(rec, entry.idx)
|
|
73
|
+
if (!location.matched) {
|
|
74
|
+
regions.push({ callId: rec.callId, idx: entry.idx, stale: true, status, create: false, oldLines: entry.hunk.oldText === null ? [] : entry.hunk.oldText.split('\n'), newLines: entry.hunk.newText ? entry.hunk.newText.split('\n') : [], rec, superseded: rec.superseded === true })
|
|
69
75
|
continue
|
|
70
76
|
}
|
|
71
|
-
const start = countLinesBefore(content,
|
|
72
|
-
const oldLines =
|
|
73
|
-
const newLines = newText.split('\n')
|
|
77
|
+
const start = countLinesBefore(content, location.start) + 1
|
|
78
|
+
const oldLines = entry.hunk.oldText === null ? [] : entry.hunk.oldText.split('\n')
|
|
79
|
+
const newLines = entry.hunk.newText.split('\n')
|
|
74
80
|
const trimmed = trimCommonLines(oldLines, newLines)
|
|
75
81
|
const regionStart = start + trimmed.shift
|
|
76
|
-
regions.push({ callId: rec.callId, idx:
|
|
82
|
+
regions.push({ callId: rec.callId, idx: entry.idx, start: regionStart, end: regionStart + trimmed.newLines.length, oldLines: trimmed.oldLines, newLines: trimmed.newLines, status, create: false, rec, superseded: rec.superseded === true })
|
|
77
83
|
}
|
|
78
84
|
}
|
|
79
85
|
regions.sort((a, b) => (a.start ?? Infinity) - (b.start ?? Infinity))
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/* dsh-vscode-mode MCP 管理界面样式。
|
|
2
|
+
全部使用 DSH 真实主题变量(--dsw-alias-*),不再用不存在的 fg-* 变量。 */
|
|
3
|
+
.vsm-mcp-page{display:flex;flex-direction:column;gap:18px;width:100%;max-width:920px;box-sizing:border-box;color:var(--dsw-alias-label-primary,#e6e6e6);padding-bottom:32px}
|
|
4
|
+
|
|
5
|
+
/* 顶部:标题 + 副标题 + 添加按钮 */
|
|
6
|
+
.vsm-mcp-header{display:flex;align-items:flex-start;justify-content:space-between;gap:20px}
|
|
7
|
+
.vsm-mcp-header h2{margin:0 0 4px;font-size:20px;font-weight:600;line-height:1.35;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
8
|
+
.vsm-mcp-header p{margin:0;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px;line-height:1.5}
|
|
9
|
+
.vsm-primary{border:0;border-radius:6px;padding:8px 16px;background:var(--dsw-alias-button-primary-fill,#6d55ff);color:var(--dsw-alias-button-contrast-fill,#fff);font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap}
|
|
10
|
+
.vsm-primary:hover{background:var(--dsw-alias-button-primary-hover,#5a46e6)}
|
|
11
|
+
.vsm-primary:disabled{opacity:.55;cursor:default}
|
|
12
|
+
|
|
13
|
+
/* 子 Tab:我的 MCP / MCP 市场 */
|
|
14
|
+
.vsm-mcp-tabs{display:flex;gap:24px;border-bottom:1px solid var(--dsw-alias-border-l2,#3a3a3a)}
|
|
15
|
+
.vsm-mcp-tabs button{border:0;background:none;color:var(--dsw-alias-label-tertiary,#9aa5b1);padding:9px 2px 11px;cursor:pointer;font-size:14px;position:relative}
|
|
16
|
+
.vsm-mcp-tabs button:hover{color:var(--dsw-alias-label-secondary,#c8c8c8)}
|
|
17
|
+
.vsm-mcp-tabs button.active{color:var(--dsw-alias-label-primary,#fff);font-weight:500}
|
|
18
|
+
.vsm-mcp-tabs button.active::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--dsw-alias-brand-primary,#6d55ff);border-radius:2px}
|
|
19
|
+
|
|
20
|
+
/* 服务卡片 */
|
|
21
|
+
.vsm-mcp-card{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:14px 16px;margin-bottom:10px;background:var(--dsw-alias-bg-layer-2,transparent);transition:border-color .15s}
|
|
22
|
+
.vsm-mcp-card:hover{border-color:var(--dsw-alias-border-l3,#555)}
|
|
23
|
+
.vsm-mcp-card-head{display:flex;justify-content:space-between;align-items:center;gap:12px}
|
|
24
|
+
.vsm-mcp-title{display:flex;align-items:center;gap:9px;font-weight:600;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6);min-width:0}
|
|
25
|
+
.vsm-mcp-source{font-size:11px;font-weight:400;padding:2px 7px;border-radius:4px;background:var(--dsw-alias-bg-mask-2,#ffffff22);color:var(--dsw-alias-label-tertiary,#9aa5b1);flex-shrink:0}
|
|
26
|
+
.vsm-mcp-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;background:var(--dsw-alias-state-error-primary,#e05252)}
|
|
27
|
+
.vsm-mcp-dot.connected{background:var(--dsw-alias-state-success-primary,#38bd75)}
|
|
28
|
+
.vsm-mcp-dot.connecting{background:var(--dsw-alias-state-warn-primary,#e5b93f)}
|
|
29
|
+
.vsm-mcp-dot.disabled{background:var(--dsw-alias-label-tertiary,#9aa5b1)}
|
|
30
|
+
.vsm-mcp-actions{display:flex;align-items:center;gap:4px;flex-shrink:0}
|
|
31
|
+
.vsm-mcp-actions button{border:0;background:none;color:var(--dsw-alias-label-tertiary,#9aa5b1);cursor:pointer;font-size:15px;width:28px;height:28px;border-radius:6px;line-height:1}
|
|
32
|
+
.vsm-mcp-actions button:hover{color:var(--dsw-alias-label-primary,#fff);background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
|
|
33
|
+
.vsm-mcp-actions button.vsm-danger:hover{color:var(--dsw-alias-state-error-primary,#f07878);background:var(--dsw-alias-interactive-bg-hover-danger,#ff000014)}
|
|
34
|
+
.vsm-switch{font-size:14px !important}
|
|
35
|
+
.vsm-switch.on{color:var(--dsw-alias-state-success-primary,#38bd75)}
|
|
36
|
+
.vsm-mcp-meta{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);margin:9px 0}
|
|
37
|
+
.vsm-mcp-error{color:var(--dsw-alias-state-error-primary,#f07878);font-size:12px;margin:8px 0}
|
|
38
|
+
.vsm-mcp-banner{padding:9px 12px;border:1px solid var(--dsw-alias-state-error-secondary,#d9534f55);border-radius:6px;background:var(--dsw-alias-state-error-secondary,#d9534f1a);margin-bottom:10px}
|
|
39
|
+
|
|
40
|
+
/* 工具 chips */
|
|
41
|
+
.vsm-mcp-tools{display:flex;flex-wrap:wrap;gap:6px}
|
|
42
|
+
.vsm-mcp-chip{font:12px var(--ds-font-family-code,ui-monospace,monospace);padding:4px 8px;border-radius:5px;background:var(--dsw-alias-bg-mask-2,#ffffff14);color:var(--dsw-alias-label-secondary,#c8c8c8);border:1px solid var(--dsw-alias-border-l1,#333)}
|
|
43
|
+
|
|
44
|
+
/* 空态 / 加载态 */
|
|
45
|
+
.vsm-mcp-empty{text-align:center;padding:56px 20px;color:var(--dsw-alias-label-tertiary,#9aa5b1);border:1px dashed var(--dsw-alias-border-l2,#3a3a3a);border-radius:8px;font-size:13px}
|
|
46
|
+
|
|
47
|
+
/* 项目搜索下拉框 */
|
|
48
|
+
.vsm-project-picker{position:relative;width:100%;z-index:4}
|
|
49
|
+
.vsm-project-picker-trigger{width:100%;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:8px;background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dsw-alias-label-primary,#e6e6e6);cursor:pointer;text-align:left}
|
|
50
|
+
.vsm-project-picker-trigger:hover{border-color:var(--dsw-alias-brand-primary,#6d55ff)}
|
|
51
|
+
.vsm-project-picker-label{display:flex;flex-direction:column;min-width:0;font-size:13px;font-weight:500}
|
|
52
|
+
.vsm-project-picker-label small{margin-top:3px;color:var(--dsw-alias-label-tertiary,#9aa5b1);font:11px var(--ds-font-family-code,ui-monospace,monospace);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
53
|
+
.vsm-project-picker-arrow{color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:16px}
|
|
54
|
+
.vsm-project-picker-menu{position:absolute;top:calc(100% + 6px);left:0;right:0;padding:8px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:9px;background:var(--dsw-alias-bg-overlay,#202020);box-shadow:0 12px 36px #0008}
|
|
55
|
+
.vsm-project-picker-search{width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:var(--dsw-alias-bg-layer-1,#2b2b2b);color:var(--dsw-alias-label-primary,#eee);outline:none}
|
|
56
|
+
.vsm-project-picker-search:focus{border-color:var(--dsw-alias-brand-primary,#6d55ff)}
|
|
57
|
+
.vsm-project-picker-options{max-height:260px;overflow:auto;margin-top:6px}
|
|
58
|
+
.vsm-project-option{display:flex;width:100%;flex-direction:column;gap:3px;padding:8px 9px;border:0;border-radius:6px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;text-align:left}
|
|
59
|
+
.vsm-project-option:hover,.vsm-project-option.highlight{background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
|
|
60
|
+
.vsm-project-option.selected{color:var(--dsw-alias-brand-primary,#6d55ff)}
|
|
61
|
+
.vsm-project-option small{overflow:hidden;color:var(--dsw-alias-label-tertiary,#9aa5b1);font:11px var(--ds-font-family-code,ui-monospace,monospace);text-overflow:ellipsis;white-space:nowrap}
|
|
62
|
+
.vsm-project-picker-empty{padding:18px 8px;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:12px;text-align:center}
|
|
63
|
+
|
|
64
|
+
/* 项目级 MCP 分组 */
|
|
65
|
+
.vsm-project{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:12px;margin-bottom:14px;overflow:hidden;background:var(--dsw-alias-bg-layer-1,transparent)}
|
|
66
|
+
.vsm-project-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l1,#333);background:var(--dsw-alias-bg-mask-1,#ffffff0a)}
|
|
67
|
+
.vsm-project-title{display:flex;align-items:center;gap:10px;min-width:0}
|
|
68
|
+
.vsm-project-icon{color:var(--dsw-alias-brand-primary,#6d55ff);font-size:15px;flex-shrink:0}
|
|
69
|
+
.vsm-project-name{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6);display:block}
|
|
70
|
+
.vsm-project-path{font-size:11px;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-family:var(--ds-font-family-code,ui-monospace,monospace);display:block;margin-top:2px;word-break:break-all}
|
|
71
|
+
.vsm-primary.vsm-small{padding:5px 12px;font-size:12px}
|
|
72
|
+
.vsm-project-empty{text-align:center;padding:26px 16px;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:12px}
|
|
73
|
+
.vsm-project .vsm-mcp-card{border:0;border-radius:0;margin:0;border-bottom:1px solid var(--dsw-alias-border-l1,#333);background:transparent}
|
|
74
|
+
.vsm-project .vsm-mcp-card:last-child{border-bottom:0}
|
|
75
|
+
.vsm-project .vsm-mcp-banner{border-radius:0;margin:0}
|
|
76
|
+
|
|
77
|
+
/* 新增 MCP 弹窗 */
|
|
78
|
+
.vsm-mcp-modal{position:fixed;inset:0;background:var(--dsw-alias-bg-mask-drop,#000000aa);display:flex;align-items:center;justify-content:center;z-index:50}
|
|
79
|
+
.vsm-mcp-dialog{width:min(560px,calc(100vw - 40px));max-height:88vh;overflow:auto;padding:22px 24px;border-radius:12px;background:var(--dsw-alias-bg-overlay,#202020);border:1px solid var(--dsw-alias-border-l2,#444);box-shadow:0 16px 60px #000c;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
80
|
+
.vsm-mcp-dialog h3{margin:0 0 14px;font-size:16px;font-weight:600}
|
|
81
|
+
.vsm-mcp-dialog label{display:flex;flex-direction:column;gap:6px;margin:12px 0;font-size:12px;color:var(--dsw-alias-label-secondary,#aaa)}
|
|
82
|
+
.vsm-mcp-dialog input,.vsm-mcp-dialog select,.vsm-mcp-dialog textarea{font:inherit;color:var(--dsw-alias-label-primary,#eee);background:var(--dsw-alias-bg-layer-1,#2b2b2b);border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;padding:8px 10px}
|
|
83
|
+
.vsm-mcp-dialog input:focus,.vsm-mcp-dialog select:focus,.vsm-mcp-dialog textarea:focus{outline:none;border-color:var(--dsw-alias-brand-primary,#6d55ff)}
|
|
84
|
+
.vsm-mcp-dialog textarea{min-height:64px;resize:vertical;font-family:var(--ds-font-family-code,ui-monospace,monospace)}
|
|
85
|
+
.vsm-mcp-dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:20px}
|
|
86
|
+
.vsm-mcp-dialog-actions button{padding:8px 14px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;font-size:13px}
|
|
87
|
+
.vsm-mcp-dialog-actions button:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
|
package/src/client/ui/DiffBox.ts
CHANGED
|
@@ -37,6 +37,7 @@ export function DiffBox(props) {
|
|
|
37
37
|
})
|
|
38
38
|
const stale = staleRegions.map((r) => React.createElement('div', { key: 'stale' + callIdAttr(r.callId, r.idx), className: 'edrv-diffrow edrv-diffrow-stale', title: '差异无法定位(文件可能已被手动修改)' },
|
|
39
39
|
React.createElement('span', { className: 'edrv-diffrow-l' }, 'L' + (r.start ?? '?') + ' 无法定位'),
|
|
40
|
+
React.createElement('span', { className: 'edrv-diffrow-tag' }, '冲突/需刷新'),
|
|
40
41
|
React.createElement('span', { className: 'edrv-diffrow-tag' }, badgeOf(r.status))))
|
|
41
42
|
let othersEl = null
|
|
42
43
|
if (otherFiles.length) {
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — VSCodeMode 设置区内的 MCP 管理。
|
|
4
|
+
* 子 Tab:我的 MCP(profile 全局)/ 项目 MCP(各项目 .mcp.json)/ MCP 市场(占位)。
|
|
5
|
+
* 作者 ddj 2026年08月22号
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react'
|
|
8
|
+
import { rpc } from '../rpc.js'
|
|
9
|
+
import type { MpcProject, MpcServer } from '../../shared/mcp.js'
|
|
10
|
+
import '../styles/mcp.css'
|
|
11
|
+
|
|
12
|
+
const EMPTY = { serverName: '', transport: 'stdio', command: '', args: '', cwd: '', url: '', headers: '' }
|
|
13
|
+
|
|
14
|
+
/** 将表单草稿转换为 Host MCP 配置。 */
|
|
15
|
+
function configOf(draft) {
|
|
16
|
+
const config = { serverName: draft.serverName.trim(), transport: draft.transport }
|
|
17
|
+
if (draft.transport === 'stdio') {
|
|
18
|
+
config.command = draft.command.trim()
|
|
19
|
+
config.args = draft.args.split(/\r?\n|\s+/).filter(Boolean)
|
|
20
|
+
if (draft.cwd.trim()) config.cwd = draft.cwd.trim()
|
|
21
|
+
} else {
|
|
22
|
+
config.url = draft.url.trim()
|
|
23
|
+
config.headers = parsePairs(draft.headers)
|
|
24
|
+
}
|
|
25
|
+
return config
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 解析每行 key=value 的表单字段。 */
|
|
29
|
+
function parsePairs(text) {
|
|
30
|
+
return Object.fromEntries(text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
31
|
+
const idx = line.indexOf('=')
|
|
32
|
+
return idx > 0 ? [line.slice(0, idx).trim(), line.slice(idx + 1).trim()] : [line, '']
|
|
33
|
+
}))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 服务状态文案。 */
|
|
37
|
+
function statusOf(server) {
|
|
38
|
+
return server.status === 'connected' ? '在线' : server.status === 'connecting' ? '连接中' : server.status === 'disabled' ? '已禁用' : '错误'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** MCP 服务卡片(全局与项目共用)。 */
|
|
42
|
+
function ServerCard({ server, onRefresh, onToggle, onRemove }) {
|
|
43
|
+
return React.createElement('article', { className: 'vsm-mcp-card' },
|
|
44
|
+
React.createElement('div', { className: 'vsm-mcp-card-head' },
|
|
45
|
+
React.createElement('div', { className: 'vsm-mcp-title' }, React.createElement('span', { className: 'vsm-mcp-dot ' + (server.enabled ? server.status : 'disabled') }), server.serverName),
|
|
46
|
+
React.createElement('div', { className: 'vsm-mcp-actions' },
|
|
47
|
+
React.createElement('button', { onClick: () => onRefresh(server.id), title: '刷新连接' }, '↻'),
|
|
48
|
+
React.createElement('button', { className: 'vsm-danger', onClick: () => onRemove(server.id), title: '删除 MCP' }, '⌫'),
|
|
49
|
+
React.createElement('button', { className: 'vsm-switch ' + (server.enabled ? 'on' : ''), onClick: () => onToggle(server), 'aria-label': server.enabled ? '禁用' : '启用' }, server.enabled ? '●' : '○'),
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
React.createElement('div', { className: 'vsm-mcp-meta' }, statusOf(server), ' · ', server.toolCount, ' 个工具 · ', server.transport),
|
|
53
|
+
server.error && React.createElement('div', { className: 'vsm-mcp-error' }, server.error),
|
|
54
|
+
React.createElement('div', { className: 'vsm-mcp-tools' }, server.tools.map((tool) => React.createElement('span', { key: tool.name, className: 'vsm-mcp-chip', title: tool.description || tool.name }, tool.name))),
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 按项目标题或路径过滤,大小写不敏感。 */
|
|
59
|
+
export function filterProjects(projects, query) {
|
|
60
|
+
const needle = String(query ?? '').trim().toLowerCase()
|
|
61
|
+
if (!needle) return projects
|
|
62
|
+
return projects.filter((project) => `${project.title} ${project.workspacePath}`.toLowerCase().includes(needle))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 带搜索的项目组合框。 */
|
|
66
|
+
function ProjectPicker({ projects, value, onChange }) {
|
|
67
|
+
const [open, setOpen] = React.useState(false)
|
|
68
|
+
const [query, setQuery] = React.useState('')
|
|
69
|
+
const [highlight, setHighlight] = React.useState(0)
|
|
70
|
+
const rootRef = React.useRef(null)
|
|
71
|
+
const selected = projects.find((project) => project.workspacePath === value)
|
|
72
|
+
const filtered = React.useMemo(() => filterProjects(projects, query), [projects, query])
|
|
73
|
+
|
|
74
|
+
React.useEffect(() => {
|
|
75
|
+
const close = (event) => {
|
|
76
|
+
if (!rootRef.current?.contains(event.target)) setOpen(false)
|
|
77
|
+
}
|
|
78
|
+
document.addEventListener('mousedown', close)
|
|
79
|
+
return () => document.removeEventListener('mousedown', close)
|
|
80
|
+
}, [])
|
|
81
|
+
|
|
82
|
+
const choose = (project) => {
|
|
83
|
+
onChange(project.workspacePath)
|
|
84
|
+
setQuery('')
|
|
85
|
+
setOpen(false)
|
|
86
|
+
}
|
|
87
|
+
const move = (delta) => setHighlight((old) => Math.max(0, Math.min(Math.max(0, filtered.length - 1), old + delta)))
|
|
88
|
+
const keyDown = (event) => {
|
|
89
|
+
if (event.key === 'ArrowDown') { event.preventDefault(); move(1) }
|
|
90
|
+
else if (event.key === 'ArrowUp') { event.preventDefault(); move(-1) }
|
|
91
|
+
else if (event.key === 'Enter' && filtered[highlight]) { event.preventDefault(); choose(filtered[highlight]) }
|
|
92
|
+
else if (event.key === 'Escape') { event.preventDefault(); setOpen(false) }
|
|
93
|
+
}
|
|
94
|
+
return React.createElement('div', { className: 'vsm-project-picker', ref: rootRef },
|
|
95
|
+
React.createElement('button', { className: 'vsm-project-picker-trigger', role: 'combobox', 'aria-expanded': open, onClick: () => setOpen((old) => !old) },
|
|
96
|
+
React.createElement('span', { className: 'vsm-project-picker-label' }, selected?.title || '选择项目', selected && React.createElement('small', null, selected.workspacePath)),
|
|
97
|
+
React.createElement('span', { className: 'vsm-project-picker-arrow' }, open ? '⌃' : '⌄'),
|
|
98
|
+
),
|
|
99
|
+
open && React.createElement('div', { className: 'vsm-project-picker-menu' },
|
|
100
|
+
React.createElement('input', { autoFocus: true, className: 'vsm-project-picker-search', value: query, placeholder: '搜索项目名称或路径…', onChange: (event) => { setQuery(event.target.value); setHighlight(0) }, onKeyDown: keyDown }),
|
|
101
|
+
React.createElement('div', { className: 'vsm-project-picker-options', role: 'listbox' }, filtered.length ? filtered.map((project, index) => React.createElement('button', { key: project.workspacePath, role: 'option', 'aria-selected': project.workspacePath === value, className: 'vsm-project-option ' + (index === highlight ? 'highlight' : '') + (project.workspacePath === value ? ' selected' : ''), onMouseEnter: () => setHighlight(index), onClick: () => choose(project) }, React.createElement('span', null, project.title), React.createElement('small', null, project.workspacePath, ' · ', project.servers.length, ' 个 MCP'))) : React.createElement('div', { className: 'vsm-project-picker-empty' }, '没有匹配的项目')),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 单个项目的 MCP 分组。 */
|
|
107
|
+
function ProjectGroup({ project, busy, onAdd, onRefresh, onToggle, onRemove }) {
|
|
108
|
+
const head = React.createElement('div', { className: 'vsm-project-head' },
|
|
109
|
+
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))),
|
|
110
|
+
React.createElement('button', { className: 'vsm-primary vsm-small', onClick: () => onAdd(project) }, '+ 添加 MCP'),
|
|
111
|
+
)
|
|
112
|
+
let body
|
|
113
|
+
if (project.missingDir) body = React.createElement('div', { className: 'vsm-project-empty' }, '项目目录已不存在,无法管理 MCP')
|
|
114
|
+
else if (project.fileError) body = React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, project.fileError)
|
|
115
|
+
else if (!project.servers.length) body = React.createElement('div', { className: 'vsm-project-empty' }, '此项目未配置 MCP')
|
|
116
|
+
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) }))
|
|
117
|
+
return React.createElement('section', { className: 'vsm-project' }, head, body)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** MCP 管理主面板。 */
|
|
121
|
+
export function McpSettings() {
|
|
122
|
+
const [servers, setServers] = React.useState<MpcServer[]>([])
|
|
123
|
+
const [projects, setProjects] = React.useState<MpcProject[]>([])
|
|
124
|
+
const [loading, setLoading] = React.useState(true)
|
|
125
|
+
const [busy, setBusy] = React.useState('')
|
|
126
|
+
const [error, setError] = React.useState('')
|
|
127
|
+
const [tab, setTab] = React.useState('mine')
|
|
128
|
+
const [draft, setDraft] = React.useState(EMPTY)
|
|
129
|
+
const [showForm, setShowForm] = React.useState(false)
|
|
130
|
+
const [projectForm, setProjectForm] = React.useState(null)
|
|
131
|
+
const [selectedPath, setSelectedPath] = React.useState('')
|
|
132
|
+
|
|
133
|
+
React.useEffect(() => {
|
|
134
|
+
if (!projects.length) {
|
|
135
|
+
setSelectedPath('')
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
if (!projects.some((project) => project.workspacePath === selectedPath)) setSelectedPath(projects[0].workspacePath)
|
|
139
|
+
}, [projects, selectedPath])
|
|
140
|
+
|
|
141
|
+
const loadAll = React.useCallback(() => {
|
|
142
|
+
setLoading(true)
|
|
143
|
+
setError('')
|
|
144
|
+
Promise.all([rpc('mcp.list', {}), rpc('mcp.projects', {})]).then(([list, prj]) => {
|
|
145
|
+
if (!list.ok) throw new Error(list.error)
|
|
146
|
+
if (!prj.ok) throw new Error(prj.error)
|
|
147
|
+
setServers(list.servers)
|
|
148
|
+
setProjects(prj.projects)
|
|
149
|
+
}).catch((e) => setError(String(e))).finally(() => setLoading(false))
|
|
150
|
+
}, [])
|
|
151
|
+
|
|
152
|
+
React.useEffect(loadAll, [loadAll])
|
|
153
|
+
|
|
154
|
+
const finish = (result) => {
|
|
155
|
+
if (!result.ok) throw new Error(result.error)
|
|
156
|
+
return result
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const action = (label, id, method, args, patch) => {
|
|
160
|
+
setBusy(id)
|
|
161
|
+
setError('')
|
|
162
|
+
return rpc(method, args).then(finish).then((result) => {
|
|
163
|
+
if (patch) patch(result)
|
|
164
|
+
}).catch((e) => setError(String(e))).finally(() => setBusy('')).then(() => label)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const refreshGlobal = (id) => action('refresh', id, 'mcp.refresh', { id }, (r) => setServers((old) => old.map((s) => s.id === id ? r.server : s)))
|
|
168
|
+
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)))
|
|
169
|
+
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)))
|
|
170
|
+
|
|
171
|
+
const replaceProject = (workspacePath) => (result) => setProjects((old) => old.map((p) => p.workspacePath === workspacePath ? result.project : p))
|
|
172
|
+
const projectAction = (workspacePath, serverName, method, args, patch) => {
|
|
173
|
+
setBusy(workspacePath + ':' + serverName)
|
|
174
|
+
setError('')
|
|
175
|
+
return rpc(method, args).then(finish).then((result) => {
|
|
176
|
+
if (patch) patch(result)
|
|
177
|
+
}).catch((e) => setError(String(e))).finally(() => setBusy(''))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const refreshProject = (p, s) => projectAction(p.workspacePath, s.serverName, 'mcp.projectRefresh', { workspacePath: p.workspacePath, serverName: s.serverName }, replaceProject(p.workspacePath))
|
|
181
|
+
const toggleProject = (p, s) => projectAction(p.workspacePath, s.serverName, 'mcp.projectToggle', { workspacePath: p.workspacePath, serverName: s.serverName, enabled: !s.enabled }, replaceProject(p.workspacePath))
|
|
182
|
+
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))
|
|
183
|
+
|
|
184
|
+
const saveGlobal = () => {
|
|
185
|
+
setBusy('save')
|
|
186
|
+
setError('')
|
|
187
|
+
rpc('mcp.save', { config: configOf(draft) }).then(finish).then((result) => {
|
|
188
|
+
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))
|
|
189
|
+
setDraft(EMPTY)
|
|
190
|
+
setShowForm(false)
|
|
191
|
+
}).catch((e) => setError(String(e))).finally(() => setBusy(''))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const saveProject = () => {
|
|
195
|
+
if (!projectForm) return
|
|
196
|
+
setBusy('project-save')
|
|
197
|
+
setError('')
|
|
198
|
+
const config = configOf(draft)
|
|
199
|
+
rpc('mcp.projectSave', { workspacePath: projectForm.workspacePath, serverName: config.serverName, config }).then(finish).then((result) => {
|
|
200
|
+
setProjects((old) => old.map((p) => p.workspacePath === projectForm.workspacePath ? result.project : p))
|
|
201
|
+
setDraft(EMPTY)
|
|
202
|
+
setProjectForm(null)
|
|
203
|
+
}).catch((e) => setError(String(e))).finally(() => setBusy(''))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const edit = (key, value) => setDraft((old) => ({ ...old, [key]: value }))
|
|
207
|
+
const marketBody = React.createElement('div', { className: 'vsm-mcp-empty' }, 'MCP 市场暂未接入')
|
|
208
|
+
const selectedProject = projects.find((project) => project.workspacePath === selectedPath)
|
|
209
|
+
const projectBody = projects.length ? React.createElement(React.Fragment, null,
|
|
210
|
+
React.createElement(ProjectPicker, { projects, value: selectedPath, onChange: setSelectedPath }),
|
|
211
|
+
selectedProject ? React.createElement(ProjectGroup, { project: selectedProject, busy, onAdd: (project) => { setDraft(EMPTY); setProjectForm({ workspacePath: project.workspacePath, title: project.title }) }, onRefresh: refreshProject, onToggle: toggleProject, onRemove: removeProject }) : React.createElement('div', { className: 'vsm-mcp-empty' }, '请选择项目'),
|
|
212
|
+
) : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有项目')
|
|
213
|
+
let body
|
|
214
|
+
if (loading) body = React.createElement('div', { className: 'vsm-mcp-empty' }, '正在读取 MCP 服务…')
|
|
215
|
+
else if (tab === 'mine') body = React.createElement('div', null, servers.length ? servers.map((s) => React.createElement(ServerCard, { key: s.id, server: s, onRefresh: refreshGlobal, onToggle: toggleGlobal, onRemove: removeGlobal })) : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有配置全局 MCP'))
|
|
216
|
+
else if (tab === 'projects') body = projectBody
|
|
217
|
+
else body = marketBody
|
|
218
|
+
const form = showForm ? React.createElement(McpForm, { title: '添加全局 MCP', draft, busy, edit, save: saveGlobal, close: () => setShowForm(false) }) : projectForm ? React.createElement(McpForm, { title: '添加项目 MCP · ' + projectForm.title, draft, busy, edit, save: saveProject, close: () => setProjectForm(null) }) : null
|
|
219
|
+
return React.createElement('section', { className: 'vsm-mcp-page' },
|
|
220
|
+
React.createElement('header', { className: 'vsm-mcp-header' }, React.createElement('div', null, React.createElement('h2', null, 'VSCodeMode'), React.createElement('p', null, '管理当前 profile 与各项目的 Model Context Protocol 服务。')), React.createElement('button', { className: 'vsm-primary', onClick: () => { setDraft(EMPTY); setShowForm(true) } }, '+ 添加全局 MCP')),
|
|
221
|
+
React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'mine' ? 'active' : '', onClick: () => setTab('mine') }, '我的 MCP'), React.createElement('button', { className: tab === 'projects' ? 'active' : '', onClick: () => setTab('projects') }, '项目 MCP'), React.createElement('button', { className: tab === 'market' ? 'active' : '', onClick: () => setTab('market') }, 'MCP 市场')),
|
|
222
|
+
error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
|
|
223
|
+
body,
|
|
224
|
+
form,
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** MCP 新增表单(全局/项目共用)。 */
|
|
229
|
+
function McpForm({ title, draft, busy, edit, save, close }) {
|
|
230
|
+
const input = (key, placeholder) => React.createElement('input', { value: draft[key], onChange: (e) => edit(key, e.target.value), placeholder })
|
|
231
|
+
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) })))
|
|
232
|
+
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 }, '保存并连接'))))
|
|
233
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
import { registerRoutes } from './routes.js'
|
|
9
9
|
import { captureToolResult } from './capture.js'
|
|
10
10
|
import { handleRpc } from './rpc.js'
|
|
11
|
+
import { installIsolation } from './mcpIsolation.js'
|
|
11
12
|
import { dropFileIndex } from './workspace.js'
|
|
12
13
|
import { cwdOf } from './registry.js'
|
|
13
14
|
import type { Registry } from './registry.js'
|
|
14
15
|
import type { Ctx } from './store.js'
|
|
15
16
|
|
|
16
17
|
export const name = "dsh-vscode-mode"
|
|
17
|
-
export const inject = ['sessions', 'fs', 'webServer']
|
|
18
|
+
export const inject = ['sessions', 'fs', 'webServer', 'loader', 'tools', 'workspaceRegistry', 'agents']
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* 装配插件:挂事件监听、注册路由。
|
|
@@ -38,6 +39,7 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
38
39
|
})
|
|
39
40
|
|
|
40
41
|
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args))
|
|
42
|
+
installIsolation(ctx)
|
|
41
43
|
|
|
42
|
-
ctx.logger?.info?.('[' + name + '] 编辑差异审查已装配(/edrv/rpc
|
|
44
|
+
ctx.logger?.info?.('[' + name + '] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用)')
|
|
43
45
|
}
|