dsh-vscode-mode 0.1.12 → 0.1.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.12",
3
+ "version": "0.1.15",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 中央编辑器(文件页签/QuickOpen/状态栏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
@@ -39,8 +39,14 @@
39
39
  "cordis.patch.yml"
40
40
  ],
41
41
  "license": "BSD-3-Clause",
42
+ "dependencies": {
43
+ "@vscode/ripgrep": "^1.18.0"
44
+ },
42
45
  "peerDependencies": {
43
46
  "@deepseek-ai/dsh-client-ui-slots": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0",
47
+ "@deepseek-ai/dsh-settings": ">=0.1.0-rc.1 <0.2.0-0",
48
+ "@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.1 <0.2.0-0",
49
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.1 <0.2.0-0",
44
50
  "@deepseek-ai/dsh-llm": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0",
45
51
  "@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0",
46
52
  "cordis": ">=4.0.0-rc <5",
@@ -48,9 +54,11 @@
48
54
  "schemastery": "^3.18.0"
49
55
  },
50
56
  "devDependencies": {
57
+ "@deepseek-ai/dsh-settings": "0.1.0-rc.8",
51
58
  "@types/node": "^24.13.3",
52
59
  "@types/react": "^18.3.1",
53
60
  "react": "^18.3.1",
61
+ "schemastery": "3.18.0",
54
62
  "tsdown": "^0.22.14",
55
63
  "typescript": "^5.9.0",
56
64
  "vitest": "^3.2.4"
package/src/capture.ts CHANGED
@@ -6,8 +6,8 @@
6
6
  import type { DiffRecord, Hunk } from './shared/types.js'
7
7
  import { annotateHunks, fingerprint } from './shared/diff.js'
8
8
  import type { Ctx, Session } from './store.js'
9
- import { archiveRecords, loadBucket, saveBucket } from './store.js'
10
- import { fileMaxBatch, prune, recordResolved } from './model.js'
9
+ import { loadBucket, saveBucket } from './store.js'
10
+ import { fileMaxBatch, prune } from './model.js'
11
11
  import type { Registry } from './registry.js'
12
12
  import { cwdOf } from './registry.js'
13
13
 
@@ -93,14 +93,8 @@ export async function captureToolResult(ctx: Ctx, registry: Registry, exec: any,
93
93
  if (!bucket) { bucket = await loadBucket(ctx, cwd); registry.set(cwd, bucket) }
94
94
  record.batch = fileMaxBatch(bucket, path) + 1
95
95
  bucket.set(exec.callId, record)
96
- // 融合:文件被再次修改(batch 递增)时,早于最新批次的未归档差异一律归档(内容已含其效果/被取代)
97
- const prior: DiffRecord[] = []
98
- for (const r of bucket.values()) {
99
- if (r.path === path && r.callId !== exec.callId && !r.archived && (r.batch ?? 0) < record.batch) prior.push(r)
100
- }
101
- if (prior.length) {
102
- await archiveRecords(ctx, session, cwd, bucket, prior, prior.every(recordResolved) ? '已处理且被后续修改取代' : '被后续修改融合(内容已含其效果)')
103
- }
96
+ // 同一文件的多次 edit/write 都保留为独立记录,便于逐条审查。
97
+ // 旧实现按文件 batch 自动归档 prior,会导致界面看起来每个文件只剩一条差异。
104
98
  prune(bucket)
105
99
  await saveBucket(ctx, cwd, bucket, session)
106
100
  } catch (error) {
@@ -0,0 +1,172 @@
1
+ /**
2
+ * dsh-vscode-mode client — 文件打开器注册表与自动选择。
3
+ * @author ddj 2026年08月24号
4
+ */
5
+
6
+ export const AUTO_OPEN_TOOL = 'auto'
7
+ export const SYSTEM_OPEN_TOOL = 'system'
8
+ export const VSCODE_OPEN_TOOL = 'dsh-vscode-mode'
9
+ export const SIDEBAR_OPEN_TOOL = 'dsh-better-sidebar'
10
+
11
+ export interface FileOpenContext {
12
+ sessionId?: string
13
+ cwd?: string
14
+ }
15
+
16
+ export interface FileOpener {
17
+ id: string
18
+ label: string
19
+ description?: string
20
+ priority?: number
21
+ isAvailable?: () => boolean
22
+ open: (path: string, context: FileOpenContext) => void | Promise<void>
23
+ }
24
+
25
+ export interface FileOpenerRegistry {
26
+ register(opener: FileOpener): () => void
27
+ registerFileOpener(opener: FileOpener): () => void
28
+ list(): readonly FileOpener[]
29
+ subscribe(listener: () => void): () => void
30
+ get(id: string): FileOpener | undefined
31
+ }
32
+
33
+ /**
34
+ * 写入外部打开器并触发注册表通知。
35
+ * @author ddj 2026年08月24号
36
+ * @param entries 打开器存储
37
+ * @param notify 变更通知
38
+ * @param opener 打开器
39
+ */
40
+ function registryRegister(entries: Map<string, FileOpener>, notify: () => void, opener: FileOpener): void {
41
+ if (!opener.id || typeof opener.open !== 'function') throw new TypeError('文件打开器必须提供 id 和 open')
42
+ entries.set(opener.id, opener)
43
+ notify()
44
+ }
45
+
46
+ /**
47
+ * 创建生命周期独立的文件打开器注册表。
48
+ * @author ddj 2026年08月24号
49
+ * @returns 文件打开器注册表
50
+ */
51
+ export function createFileOpenerRegistry(): FileOpenerRegistry {
52
+ const entries = new Map<string, FileOpener>()
53
+ const listeners = new Set<() => void>()
54
+ const notify = (): void => {
55
+ for (const listener of listeners) listener()
56
+ }
57
+ const list = (): readonly FileOpener[] => [...entries.values()].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
58
+ return {
59
+ register(opener: FileOpener): () => void {
60
+ registryRegister(entries, notify, opener)
61
+ return () => {
62
+ if (entries.get(opener.id) !== opener) return
63
+ entries.delete(opener.id)
64
+ notify()
65
+ }
66
+ },
67
+ registerFileOpener(opener: FileOpener): () => void {
68
+ registryRegister(entries, notify, opener)
69
+ return () => {
70
+ if (entries.get(opener.id) !== opener) return
71
+ entries.delete(opener.id)
72
+ notify()
73
+ }
74
+ },
75
+ list,
76
+ subscribe(listener: () => void): () => void {
77
+ listeners.add(listener)
78
+ return () => listeners.delete(listener)
79
+ },
80
+ get: (id: string) => entries.get(id),
81
+ }
82
+ }
83
+
84
+ /**
85
+ * 检查打开器当前是否可用。
86
+ * @author ddj 2026年08月24号
87
+ * @param opener 待检查打开器
88
+ * @returns 是否可用
89
+ */
90
+ export function isAvailable(opener: FileOpener): boolean {
91
+ try {
92
+ return opener.isAvailable ? opener.isAvailable() : true
93
+ } catch {
94
+ return false
95
+ }
96
+ }
97
+
98
+ /**
99
+ * 返回当前可展示的文件打开器。
100
+ * @author ddj 2026年08月24号
101
+ * @param registry 文件打开器注册表
102
+ * @returns 可用打开器列表
103
+ */
104
+ export function availableOpeners(registry: FileOpenerRegistry): FileOpener[] {
105
+ return registry.list().filter(isAvailable)
106
+ }
107
+
108
+ /**
109
+ * 按设置选择打开器,不可用时回退自动优先级。
110
+ * @author ddj 2026年08月24号
111
+ * @param registry 文件打开器注册表
112
+ * @param selected 用户选择的打开器 id
113
+ * @returns 选中的打开器
114
+ */
115
+ export function selectOpener(
116
+ registry: FileOpenerRegistry,
117
+ selected: string,
118
+ ): FileOpener | undefined {
119
+ if (selected !== AUTO_OPEN_TOOL) {
120
+ const requested = registry.get(selected)
121
+ if (requested && isAvailable(requested)) return requested
122
+ }
123
+ return availableOpeners(registry)[0]
124
+ }
125
+
126
+ /**
127
+ * 结构化探测 dsh-better-sidebar 的文件打开能力。
128
+ * @author ddj 2026年08月24号
129
+ * @param ctx 客户端服务上下文
130
+ * @returns 侧边栏打开器或 undefined
131
+ */
132
+ export function scanSidebar(ctx: { get: (name: string) => unknown }): FileOpener | undefined {
133
+ try {
134
+ const service = ctx.get('betterSidebar') as {
135
+ openFile?: (scope: { sessionId: string; cwd?: string }, path: string, title?: string) => void
136
+ isTabEnabled?: (id: string) => boolean
137
+ features?: readonly string[]
138
+ getSnapshot?: () => { state?: unknown }
139
+ } | undefined
140
+ if (!service || typeof service.openFile !== 'function') return undefined
141
+ const advertised = service.features?.includes('openFile') ?? true
142
+ if (!advertised) return undefined
143
+ const canOpen = typeof service.isTabEnabled !== 'function' || service.isTabEnabled('editor')
144
+ if (!canOpen) return undefined
145
+ return {
146
+ id: SIDEBAR_OPEN_TOOL,
147
+ label: 'dsh-better-sidebar',
148
+ description: '在 dsh-web-ui-all 侧边栏编辑器中打开',
149
+ priority: 80,
150
+ isAvailable: () => typeof service.openFile === 'function'
151
+ && (service.features?.includes('openFile') ?? true)
152
+ && (typeof service.isTabEnabled !== 'function' || service.isTabEnabled('editor')),
153
+ open: (path, context) => {
154
+ if (!context.sessionId) throw new Error('当前没有活动会话')
155
+ service.openFile!({ sessionId: context.sessionId, cwd: context.cwd }, path, baseName(path))
156
+ },
157
+ }
158
+ } catch {
159
+ return undefined
160
+ }
161
+ }
162
+
163
+ /**
164
+ * 从路径提取文件名。
165
+ * @author ddj 2026年08月24号
166
+ * @param path 文件路径
167
+ * @returns 文件名
168
+ */
169
+ export function baseName(path: string): string {
170
+ const normalized = path.replace(/\\/g, '/')
171
+ return normalized.slice(normalized.lastIndexOf('/') + 1) || path
172
+ }
@@ -16,8 +16,13 @@ import './styles/editor.css'
16
16
  import { EditorView } from './ui/EditorView.js'
17
17
  import { DiffBadge } from './ui/DiffBadge.js'
18
18
  import { McpSettings } from './ui/McpSettings.js'
19
+ import { rpc } from './rpc.js'
20
+ import { createFileOpenerRegistry, scanSidebar, type FileOpenContext } from './fileOpeners.js'
21
+ import type { FileOpenerRegistry } from './fileOpeners.js'
22
+ import { installOpenPathRouter, vscodeOpener, autoValue } from './openPathRouter.js'
23
+ import { SettingsContext, getSettingsScope } from './settingsContext.js'
19
24
 
20
- export const inject = ['slots', 'timer', 'locale', 'connection', 'remote']
25
+ export const inject = ['slots', 'timer', 'locale', 'connection', 'remote', 'workspaces', 'sessions', 'settingsScope', 'webUiSettings']
21
26
 
22
27
  /**
23
28
  * 装配客户端:注册中央编辑区视图与 header 差异角标。
@@ -27,6 +32,48 @@ export const inject = ['slots', 'timer', 'locale', 'connection', 'remote']
27
32
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
33
  export function apply(ctx: any): void {
29
34
  const schedule = (fn: () => void, ms: number) => ctx.timeout(fn, ms)
35
+ const registry: FileOpenerRegistry = createFileOpenerRegistry()
36
+ const workspaces = ctx.get('workspaces')
37
+ const sessions = ctx.get('sessions')
38
+ const originalOpenPath = workspaces?.openPath
39
+ const settings = getSettingsScope(ctx)
40
+ let selected = autoValue('auto')
41
+
42
+ ctx.provide('fileOpeners', registry)
43
+ ctx.effect(() => registry.register(vscodeOpener()), 'vscode-mode: file opener')
44
+ ctx.effect(() => registry.register({
45
+ id: 'system', label: '系统默认应用', priority: 0,
46
+ open: (path: string) => originalOpenPath.call(workspaces, path),
47
+ }), 'vscode-mode: system file opener')
48
+ ctx.effect(() => {
49
+ const sidebar = scanSidebar(ctx)
50
+ return sidebar ? registry.register(sidebar) : undefined
51
+ }, 'vscode-mode: sidebar file opener')
52
+ ctx.effect(() => {
53
+ if (!settings) return undefined
54
+ const sync = (): void => {
55
+ const snapshot = settings.getSnapshot()
56
+ if (snapshot.status === 'loading') return
57
+ selected = autoValue(snapshot.value?.fileOpenTool)
58
+ window.dispatchEvent(new CustomEvent('edrv:file-open-tool-change', { detail: { value: selected } }))
59
+ }
60
+ sync()
61
+ return settings.subscribe(sync)
62
+ }, 'vscode-mode: file opener setting sync')
63
+ if (workspaces?.openPath) {
64
+ ctx.effect(() => installOpenPathRouter({
65
+ workspaces,
66
+ registry,
67
+ selected: () => selected,
68
+ context: () => {
69
+ const current = sessions?.list?.getSnapshot?.()
70
+ const sessionId = current?.current
71
+ const summary = sessionId ? current?.byId?.[sessionId] : undefined
72
+ return { sessionId, cwd: summary?.cwd } as FileOpenContext
73
+ },
74
+ logger: (message: string) => console.warn('[dsh-vscode-mode] ' + message),
75
+ }), 'vscode-mode: file link routing')
76
+ }
30
77
 
31
78
  // 中央「文件编辑」页签:类 VSCode 编辑器(顶部=文件页签+搜索框,差异 UI=文件底部圆角悬浮框)
32
79
  ctx.slots.inject('conversation.view', () => ctx.slots.register({
@@ -49,5 +96,5 @@ export function apply(ctx: any): void {
49
96
  id: 'vscode-mode',
50
97
  order: 30,
51
98
  label: 'VSCodeMode',
52
- }, () => React.createElement(McpSettings)))
99
+ }, () => React.createElement(SettingsContext.Provider, { value: settings }, React.createElement(McpSettings, { openerRegistry: registry }))))
53
100
  }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * dsh-vscode-mode client — DSH 对话文件链接的统一打开路由。
3
+ * @author ddj 2026年08月24号
4
+ */
5
+ import { openEditorView } from './events.js'
6
+ import { AUTO_OPEN_TOOL, VSCODE_OPEN_TOOL, type FileOpenContext, type FileOpenerRegistry, isAvailable, selectOpener } from './fileOpeners.js'
7
+
8
+ export interface OpenPathRouterOptions {
9
+ workspaces: { openPath: (path: string) => Promise<void> }
10
+ registry: FileOpenerRegistry
11
+ selected: () => string
12
+ context: () => FileOpenContext
13
+ logger?: (message: string) => void
14
+ }
15
+
16
+ /**
17
+ * 安装可卸载的 workspaces.openPath 路由。
18
+ * @author ddj 2026年08月24号
19
+ * @param options 路由依赖
20
+ * @returns 恢复原方法的 disposer
21
+ */
22
+ export function installOpenPathRouter(options: OpenPathRouterOptions): () => void {
23
+ const original = options.workspaces.openPath
24
+ const open = async (path: string): Promise<void> => {
25
+ const context = options.context()
26
+ const opener = selectOpener(options.registry, options.selected())
27
+ if (!opener || !isAvailable(opener)) return original.call(options.workspaces, path)
28
+ try {
29
+ await opener.open(path, context)
30
+ } catch (error) {
31
+ options.logger?.('文件打开器 ' + opener.id + ' 失败,回退系统打开: ' + String(error))
32
+ if (opener.id !== 'system') await original.call(options.workspaces, path)
33
+ else throw error
34
+ }
35
+ }
36
+ options.workspaces.openPath = open
37
+ return () => { options.workspaces.openPath = original }
38
+ }
39
+
40
+ /**
41
+ * 创建 VSCodeMode 内置文件打开器。
42
+ * @author ddj 2026年08月24号
43
+ * @returns VSCodeMode 打开器
44
+ */
45
+ export function vscodeOpener(): { id: string; label: string; priority: number; open: (path: string) => void } {
46
+ return { id: VSCODE_OPEN_TOOL, label: 'VSCodeMode', priority: 100, open: (path) => openEditorView(path) }
47
+ }
48
+
49
+ /**
50
+ * 规范化设置中的打开器 id。
51
+ * @author ddj 2026年08月24号
52
+ * @param value 原始设置值
53
+ * @returns 可路由的打开器 id
54
+ */
55
+ export function autoValue(value: unknown): string {
56
+ return typeof value === 'string' && value.trim() ? value.trim() : AUTO_OPEN_TOOL
57
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * dsh-vscode-mode client — 设置 scope 的跨组件上下文。
3
+ * @author ddj 2026年08月24号
4
+ */
5
+ import React from 'react'
6
+
7
+ export interface SettingsScopeLike {
8
+ getSnapshot: () => { status?: string; value?: { fileOpenTool?: unknown }; writable?: boolean }
9
+ subscribe: (listener: () => void) => () => void
10
+ set: (field: string, value: unknown) => Promise<void>
11
+ }
12
+
13
+ export const SettingsContext = React.createContext<SettingsScopeLike | undefined>(undefined)
14
+
15
+ /**
16
+ * 从 Cordis 服务绑定 VSCodeMode 设置 scope。
17
+ * @author ddj 2026年08月24号
18
+ * @param ctx 客户端上下文
19
+ * @returns 设置 scope 或 undefined
20
+ */
21
+ /**
22
+ * 绑定官方设置 scope,优先使用 web-ui 兼容桥。
23
+ * @author ddj 2026年08月24号
24
+ * @param ctx 客户端服务上下文
25
+ * @returns 设置 scope;未安装设置服务时返回 undefined
26
+ */
27
+ export function getSettingsScope(ctx: { get: (name: string) => unknown }): SettingsScopeLike | undefined {
28
+ const service = ctx.get('webUiSettings') ?? ctx.get('settingsScope')
29
+ const binder = service as { bind?: (spec: { namespace: string }) => SettingsScopeLike } | undefined
30
+ return binder?.bind?.({ namespace: 'dsh-vscode-mode' })
31
+ }
@@ -17,6 +17,14 @@
17
17
  .vsm-mcp-tabs button.active{color:var(--dsw-alias-label-primary,#fff);font-weight:500}
18
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
19
 
20
+ /* 通用设置 */
21
+ .vsm-general-page{display:flex;flex-direction:column;gap:10px;max-width:720px}
22
+ .vsm-general-page h2{margin:0;font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}
23
+ .vsm-general-page p{margin:0 0 8px;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px}
24
+ .vsm-general-row{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:14px 0;border-bottom:1px solid var(--dsw-alias-border-l1,#333);color:var(--dsw-alias-label-primary,#eee);font-size:14px}
25
+ .vsm-general-row select{min-width:220px;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);font:inherit}
26
+ .vsm-general-row small{color:var(--dsw-alias-label-tertiary,#9aa5b1)}
27
+
20
28
  /* 服务卡片 */
21
29
  .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
30
  .vsm-mcp-card:hover{border-color:var(--dsw-alias-border-l3,#555)}
@@ -67,7 +67,7 @@ export function EditorView(props) {
67
67
 
68
68
  const currentRecords = React.useMemo(() => {
69
69
  const list = []
70
- for (const rec of Object.values(records)) if (rec.path === active) list.push(rec)
70
+ for (const rec of Object.values(records)) if (rec.path === active && rec.archived !== true) list.push(rec)
71
71
  return list
72
72
  }, [records, active])
73
73
 
@@ -113,7 +113,10 @@ export function EditorView(props) {
113
113
  rpc('edrv.list', { sessionId }).then((res) => {
114
114
  if (!res || !res.ok || !Array.isArray(res.records)) return
115
115
  const map = {}
116
- for (const r of res.records) map[r.callId] = r
116
+ for (const r of res.records) {
117
+ if (r.archived === true) continue
118
+ map[r.callId] = r
119
+ }
117
120
  setRecords(map)
118
121
  }).catch((e) => setError('list异常:' + String(e)))
119
122
  }
@@ -8,6 +8,8 @@ import React from 'react'
8
8
  import { rpc } from '../rpc.js'
9
9
  import type { MpcProject, MpcServer } from '../../shared/mcp.js'
10
10
  import '../styles/mcp.css'
11
+ import { availableOpeners, AUTO_OPEN_TOOL } from '../fileOpeners.js'
12
+ import { SettingsContext } from '../settingsContext.js'
11
13
 
12
14
  const EMPTY = { serverName: '', transport: 'stdio', command: '', args: '', cwd: '', url: '', headers: '' }
13
15
 
@@ -117,14 +119,49 @@ function ProjectGroup({ project, busy, onAdd, onRefresh, onToggle, onRemove }) {
117
119
  return React.createElement('section', { className: 'vsm-project' }, head, body)
118
120
  }
119
121
 
122
+ /** 通用设置:选择当前对话文件链接的打开器。 */
123
+ function GeneralSettings({ registry }) {
124
+ const [tool, setTool] = React.useState(AUTO_OPEN_TOOL)
125
+ const [busy, setBusy] = React.useState(false)
126
+ const [error, setError] = React.useState('')
127
+ const settings = React.useContext(SettingsContext)
128
+ const snapshot = settings?.getSnapshot?.()
129
+ const loading = !snapshot || snapshot.status === 'loading'
130
+ const unavailable = snapshot?.status === 'unavailable' || !settings
131
+ const notReady = snapshot?.status !== 'ready'
132
+ React.useEffect(() => {
133
+ const onChange = () => {
134
+ const next = settings?.getSnapshot?.().value?.fileOpenTool
135
+ if (typeof next === 'string') setTool(next)
136
+ }
137
+ onChange()
138
+ return settings?.subscribe?.(onChange)
139
+ }, [settings])
140
+ const save = (value) => {
141
+ setTool(value); setBusy(true); setError('')
142
+ if (!settings?.set) { setError('设置服务不可用'); setBusy(false); return }
143
+ settings.set('fileOpenTool', value).catch((e) => setError(String(e))).finally(() => setBusy(false))
144
+ }
145
+ const options = [{ id: AUTO_OPEN_TOOL, label: '自动选择', description: '按当前已注册工具自动选择' }, ...availableOpeners(registry).map((item) => ({ id: item.id, label: item.label, description: item.description }))]
146
+ const currentOption = options.some((item) => item.id === tool) ? tool : AUTO_OPEN_TOOL
147
+ return React.createElement('section', { className: 'vsm-general-page' },
148
+ React.createElement('h2', null, '通用'),
149
+ React.createElement('p', null, '配置 DSH 对话中文件链接的打开工具。工具由当前运行时自动扫描。'),
150
+ unavailable && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, '设置服务暂不可用,当前使用自动选择。'),
151
+ error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
152
+ React.createElement('label', { className: 'vsm-general-row' }, React.createElement('span', null, '文件链接使用工具'), React.createElement('select', { value: currentOption, disabled: loading || unavailable || notReady || busy || snapshot?.writable === false, onChange: (event) => save(event.target.value) }, options.map((item) => React.createElement('option', { key: item.id, value: item.id }, item.label))), loading && React.createElement('small', null, '读取中…')),
153
+ )
154
+ }
155
+
120
156
  /** MCP 管理主面板。 */
121
- export function McpSettings() {
157
+ export function McpSettings(props) {
158
+ const openerRegistry = props?.openerRegistry ?? { list: () => [] }
122
159
  const [servers, setServers] = React.useState<MpcServer[]>([])
123
160
  const [projects, setProjects] = React.useState<MpcProject[]>([])
124
161
  const [loading, setLoading] = React.useState(true)
125
162
  const [busy, setBusy] = React.useState('')
126
163
  const [error, setError] = React.useState('')
127
- const [tab, setTab] = React.useState('mine')
164
+ const [tab, setTab] = React.useState('general')
128
165
  const [draft, setDraft] = React.useState(EMPTY)
129
166
  const [showForm, setShowForm] = React.useState(false)
130
167
  const [projectForm, setProjectForm] = React.useState(null)
@@ -212,13 +249,14 @@ export function McpSettings() {
212
249
  ) : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有项目')
213
250
  let body
214
251
  if (loading) body = React.createElement('div', { className: 'vsm-mcp-empty' }, '正在读取 MCP 服务…')
252
+ else if (tab === 'general') body = React.createElement(GeneralSettings, { registry: openerRegistry })
215
253
  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
254
  else if (tab === 'projects') body = projectBody
217
255
  else body = marketBody
218
256
  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
257
  return React.createElement('section', { className: 'vsm-mcp-page' },
220
258
  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 市场')),
259
+ React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), 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
260
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
223
261
  body,
224
262
  form,
@@ -0,0 +1,72 @@
1
+ /**
2
+ * dsh-vscode-mode host — 文件链接打开工具的持久化设置。
3
+ * @author ddj 2026年08月24号
4
+ */
5
+ import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
6
+ import z from 'schemastery'
7
+ import type { Ctx } from './store.js'
8
+
9
+ export const FILE_OPEN_SETTINGS_NS = settingsNamespace('dsh-vscode-mode')
10
+ export const FILE_OPEN_DEFAULT = 'auto'
11
+ export interface FileOpenSettings { fileOpenTool: string }
12
+ export interface FileOpenSettingsState { value: string; revision?: number; update: (value: string, expectedRevision?: number) => Promise<void> }
13
+
14
+ type SettingsProvider = {
15
+ update?: (ns: string, patch: object, expectedRevision?: number) => Promise<void>
16
+ describe?: (options?: unknown) => Array<{ ns?: string; value?: unknown; revision?: number }>
17
+ get?: (ns: string) => unknown
18
+ }
19
+
20
+ const FILE_OPEN_SCHEMA = z.object({ fileOpenTool: z.string().default(FILE_OPEN_DEFAULT) })
21
+
22
+ function normalizeValue(value: unknown): string {
23
+ if (typeof value !== 'string' || value.trim() === '') return FILE_OPEN_DEFAULT
24
+ return value.trim()
25
+ }
26
+
27
+ function configValue(config: unknown): string {
28
+ return normalizeValue((config as { fileOpenTool?: unknown } | undefined)?.fileOpenTool)
29
+ }
30
+
31
+ /**
32
+ * 注册设置命名空间,并提供 host 侧的读取与更新状态。
33
+ * @author ddj 2026年08月24号
34
+ * @param ctx DSH host 上下文
35
+ * @param config 插件组合配置
36
+ * @param onChange 设置变化回调
37
+ * @returns 设置状态
38
+ */
39
+ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: string) => void): FileOpenSettingsState {
40
+ let current = configValue(config)
41
+ let revision: number | undefined
42
+ let provider: SettingsProvider | undefined
43
+ const notify = (value: unknown): void => { current = normalizeValue(value); onChange(current) }
44
+ const syncRevision = (): void => {
45
+ const descriptor = provider?.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === FILE_OPEN_SETTINGS_NS)
46
+ revision = descriptor?.revision
47
+ }
48
+ const setSource = (source: () => FileOpenSettings): void => notify(source().fileOpenTool)
49
+ const settingsChange = (): void => syncRevision()
50
+
51
+ installSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, FILE_OPEN_SCHEMA, { fileOpenTool: current }, { setSource, onChange: settingsChange })
52
+ ctx.inject?.(['settings'], (settingsCtx: Ctx) => {
53
+ provider = settingsCtx.get('settings')
54
+ syncRevision()
55
+ })
56
+
57
+ return {
58
+ get value() { return current },
59
+ get revision() { return revision },
60
+ update: async (value: string, expectedRevision?: number): Promise<void> => {
61
+ const next = normalizeValue(value)
62
+ if (!provider?.update) { notify(next); return }
63
+ await provider.update(FILE_OPEN_SETTINGS_NS, { fileOpenTool: next }, expectedRevision)
64
+ const descriptor = provider.describe?.({ redactSecrets: true })?.find((item: { ns?: string; value?: unknown }) => item.ns === FILE_OPEN_SETTINGS_NS)
65
+ const stored = descriptor?.value as { fileOpenTool?: unknown } | undefined
66
+ notify(stored?.fileOpenTool ?? next)
67
+ syncRevision()
68
+ },
69
+ }
70
+ }
71
+
72
+ export function normalizeFileOpenTool(value: unknown): string { return normalizeValue(value) }
@@ -0,0 +1,14 @@
1
+ declare module 'schemastery' {
2
+ const schema: any
3
+ export default schema
4
+ }
5
+
6
+ declare module '@deepseek-ai/dsh-settings' {
7
+ export function installSettingsSection(...args: any[]): void
8
+ export function settingsNamespace(value: string): string
9
+ }
10
+
11
+ declare module '@deepseek-ai/schemastery' {
12
+ const schema: any
13
+ export default schema
14
+ }
package/src/index.ts CHANGED
@@ -8,9 +8,11 @@
8
8
  import { registerRoutes } from './routes.js'
9
9
  import { captureToolResult } from './capture.js'
10
10
  import { handleRpc } from './rpc.js'
11
+ import { newSearcher } from './search/orchestrator.js'
11
12
  import { installIsolation } from './mcpIsolation.js'
12
13
  import { dropFileIndex } from './workspace.js'
13
14
  import { cwdOf } from './registry.js'
15
+ import { setupOpenSettings } from './fileOpenSettings.js'
14
16
  import type { Registry } from './registry.js'
15
17
  import type { Ctx } from './store.js'
16
18
 
@@ -25,6 +27,8 @@ export const inject = ['sessions', 'fs', 'webServer', 'loader', 'tools', 'worksp
25
27
  */
26
28
  export function apply(ctx: Ctx, config?: unknown): void {
27
29
  const registry: Registry = new Map()
30
+ const searcher = newSearcher(ctx)
31
+ setupOpenSettings(ctx, config, () => {})
28
32
 
29
33
  ctx.on('tools/result', (exec: unknown, result: unknown) => {
30
34
  void captureToolResult(ctx, registry, exec, result)
@@ -35,10 +39,11 @@ export function apply(ctx: Ctx, config?: unknown): void {
35
39
  if (cwd) {
36
40
  registry.delete(cwd)
37
41
  dropFileIndex(cwd)
42
+ searcher.dispose(cwd)
38
43
  }
39
44
  })
40
45
 
41
- registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args))
46
+ registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher))
42
47
  installIsolation(ctx)
43
48
 
44
49
  ctx.logger?.info?.('[' + name + '] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用)')