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/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' ? { oldText: args.old_string, newText: args.new_string } : null
38
- const synthHunk = create ? { oldText: null as string | null, newText: typeof args.content === 'string' ? args.content : (typeof value.after === 'string' ? value.after : '') } : null
39
- const hunks = metaDiffs.length ? metaDiffs : (synthHunk ? [synthHunk] : (callHunk ? [callHunk] : []))
40
- if (!hunks.length) return
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,
@@ -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 (!r.create && r.newLines && r.newLines.length) {
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.toolName === 'edit' && rec.callHunk ? rec.callHunk : h
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
- let n = 0
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.split('\n')
53
+ const lines = splitLines(content)
55
54
  for (const rec of records) {
56
- for (let i = 0; i < rec.hunks.length; i++) {
57
- const h: Hunk = rec.hunks[i]
58
- if (noopHunk(rec, h)) continue // 空差异(old===new)无实际变化,跳过
59
- const status = statusAt(rec, i)
60
- if (rec.create) {
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
- const precise = rec.toolName === 'edit' && rec.callHunk ? rec.callHunk : h
65
- const newText = precise.newText ?? ''
66
- const at = content.indexOf(newText)
67
- if (at < 0) {
68
- regions.push({ callId: rec.callId, idx: i, stale: true, status, create: false, oldLines: [], newLines: [], rec, superseded: rec.superseded === true })
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, at) + 1
72
- const oldLines = precise.oldText === null ? [] : precise.oldText.split('\n')
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: i, start: regionStart, end: regionStart + trimmed.newLines.length, oldLines: trimmed.oldLines, newLines: trimmed.newLines, status, create: false, rec, superseded: rec.superseded === true })
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)}
@@ -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
  }