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/lib/client.js +319 -7
- package/lib/client.js.map +1 -1
- package/lib/index.js +709 -91
- package/lib/index.js.map +1 -1
- package/package.json +9 -1
- package/src/capture.ts +4 -10
- package/src/client/fileOpeners.ts +172 -0
- package/src/client/index.ts +49 -2
- package/src/client/openPathRouter.ts +57 -0
- package/src/client/settingsContext.ts +31 -0
- package/src/client/styles/mcp.css +8 -0
- package/src/client/ui/EditorView.ts +5 -2
- package/src/client/ui/McpSettings.ts +41 -3
- package/src/fileOpenSettings.ts +72 -0
- package/src/host-deps.d.ts +14 -0
- package/src/index.ts +6 -1
- package/src/rpc.ts +28 -24
- package/src/search/fallback.ts +76 -0
- package/src/search/orchestrator.ts +276 -0
- package/src/search/query.ts +62 -0
- package/src/search/ranker.ts +48 -0
- package/src/search/ripgrep.ts +144 -0
- package/src/search/types.ts +47 -0
- package/src/shared/rpc.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
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 {
|
|
10
|
-
import { fileMaxBatch, prune
|
|
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
|
-
//
|
|
97
|
-
|
|
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
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -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)
|
|
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('
|
|
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 隔离已启用)')
|