dsh-vscode-mode 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/lib/client.js +432 -13
- package/lib/client.js.map +1 -1
- package/lib/index.js +418 -131
- package/lib/index.js.map +1 -1
- package/package.json +6 -1
- package/src/capture.ts +4 -10
- package/src/client/compat.ts +91 -0
- package/src/client/fileOpeners.ts +173 -0
- package/src/client/index.ts +64 -9
- package/src/client/openPathRouter.ts +57 -0
- package/src/client/settingsContext.ts +21 -0
- package/src/client/styles/mcp.css +16 -0
- package/src/client/ui/EditorView.ts +5 -2
- package/src/client/ui/McpSettings.ts +76 -3
- package/src/compat.ts +146 -0
- package/src/fileOpenSettings.ts +124 -0
- package/src/host-deps.d.ts +14 -0
- package/src/index.ts +35 -5
- package/src/mcp.ts +8 -9
- package/src/mcpIsolation.ts +6 -12
- package/src/routes.ts +26 -7
- package/src/rpc.ts +20 -0
- package/src/shared/compat.ts +20 -0
- package/src/shared/rpc.ts +7 -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.16",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 中央编辑器(文件页签/QuickOpen/状态栏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -44,6 +44,9 @@
|
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
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",
|
|
47
50
|
"@deepseek-ai/dsh-llm": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0",
|
|
48
51
|
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0",
|
|
49
52
|
"cordis": ">=4.0.0-rc <5",
|
|
@@ -51,9 +54,11 @@
|
|
|
51
54
|
"schemastery": "^3.18.0"
|
|
52
55
|
},
|
|
53
56
|
"devDependencies": {
|
|
57
|
+
"@deepseek-ai/dsh-settings": "0.1.0-rc.8",
|
|
54
58
|
"@types/node": "^24.13.3",
|
|
55
59
|
"@types/react": "^18.3.1",
|
|
56
60
|
"react": "^18.3.1",
|
|
61
|
+
"schemastery": "3.18.0",
|
|
57
62
|
"tsdown": "^0.22.14",
|
|
58
63
|
"typescript": "^5.9.0",
|
|
59
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,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 兼容层:设置桥选择 / slot 安全注册 / openPath 链式补丁 / 外部插件常量。
|
|
3
|
+
* 纯逻辑与薄 ctx 胶水,node 环境可单测(不依赖 DOM)。
|
|
4
|
+
* 作者 ddj 2026年08月24号
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** 本插件包名(与 host compat.PLUGIN_NAME 对应)。 */
|
|
8
|
+
export const PLUGIN_NAME = 'dsh-vscode-mode'
|
|
9
|
+
/** 外部侧栏插件名(dsh-web-ui-all 家族的文件打开能力提供方)。 */
|
|
10
|
+
export const SIDEBAR_PLUGIN = 'dsh-better-sidebar'
|
|
11
|
+
|
|
12
|
+
/** 设置 scope 的跨组件上下文形状(与官方 settingsScope 绑定的最小面)。 */
|
|
13
|
+
export interface SettingsScopeLike {
|
|
14
|
+
getSnapshot: () => { status?: string; value?: { fileOpenTool?: unknown }; writable?: boolean }
|
|
15
|
+
subscribe: (listener: () => void) => () => void
|
|
16
|
+
set: (field: string, value: unknown) => Promise<void>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 设置桥三级优先:webUiSettings(linxin666 兼容桥)→ settingsScope(官方)→ 无。
|
|
21
|
+
* 桥绑定抛错时降级到下一级,不让单个桥异常拖垮设置装配。
|
|
22
|
+
* @author ddj 2026年08月24号
|
|
23
|
+
* @param ctx 客户端服务上下文
|
|
24
|
+
* @returns 激活的桥名称与绑定 scope
|
|
25
|
+
*/
|
|
26
|
+
export function pickSettingsBinder(ctx: { get: (name: string) => unknown }): { service: string; scope: SettingsScopeLike | undefined } {
|
|
27
|
+
for (const service of ['webUiSettings', 'settingsScope']) {
|
|
28
|
+
try {
|
|
29
|
+
const binder = ctx.get(service) as { bind?: (spec: { namespace: string }) => SettingsScopeLike } | undefined
|
|
30
|
+
const scope = binder?.bind?.({ namespace: PLUGIN_NAME })
|
|
31
|
+
if (scope) return { service, scope }
|
|
32
|
+
} catch {
|
|
33
|
+
/* 桥异常时降级到下一级 */
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { service: 'none', scope: undefined }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** slot 注册描述(对齐 ctx.slots.register 的形状)。 */
|
|
40
|
+
export interface SlotSpec {
|
|
41
|
+
name: string
|
|
42
|
+
id: string
|
|
43
|
+
order?: number
|
|
44
|
+
label?: string
|
|
45
|
+
inject?: (sessionId: string) => object
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 跨版本 slot 安全注册:封装 ctx.slots.inject 等待声明模式(新老 DSH 通用),
|
|
50
|
+
* 服务缺失或注册抛错时降级为警告,不让单条 slot 拖垮装配。
|
|
51
|
+
* 注意:方法必须挂在服务对象上调用(slots.register 是依赖 this.records 的类方法,
|
|
52
|
+
* 解构后 this 丢失会静默失败),故禁止解构。
|
|
53
|
+
* @author ddj 2026年08月24号
|
|
54
|
+
* @param ctx 客户端上下文(slots 服务)
|
|
55
|
+
* @param spec slot 描述
|
|
56
|
+
* @param render 渲染函数
|
|
57
|
+
*/
|
|
58
|
+
export function registerSlotSafely(ctx: { slots?: { inject?: (name: string, register: () => unknown) => unknown; register?: (...args: unknown[]) => unknown } }, spec: SlotSpec, render: (props: unknown) => unknown): void {
|
|
59
|
+
const slots = ctx?.slots as { inject: (name: string, register: () => unknown) => unknown; register: (...args: unknown[]) => unknown } | undefined
|
|
60
|
+
if (!slots || typeof slots.inject !== 'function' || typeof slots.register !== 'function') {
|
|
61
|
+
console.warn('[' + PLUGIN_NAME + '] slots 服务不可用,跳过 slot ' + spec.name)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
slots.inject(spec.name, () => slots.register(spec, render))
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.warn('[' + PLUGIN_NAME + '] slot ' + spec.name + ' 注册失败(' + String(error) + '),已跳过')
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 链式补丁对象方法:保存原实现、装入包装实现,dispose 恢复。
|
|
73
|
+
* 恢复带归属校验(仅当当前实现仍是本包装时还原),多插件补丁按栈序互不踩踏。
|
|
74
|
+
* @author ddj 2026年08月24号
|
|
75
|
+
* @param owner 目标对象
|
|
76
|
+
* @param key 方法名
|
|
77
|
+
* @param wrapper 包装实现(original 为补丁前的实现)
|
|
78
|
+
* @returns 恢复函数(幂等)
|
|
79
|
+
*/
|
|
80
|
+
export function patchMethod<T extends object, K extends keyof T>(
|
|
81
|
+
owner: T,
|
|
82
|
+
key: K,
|
|
83
|
+
wrapper: (original: T[K], ...args: never[]) => unknown,
|
|
84
|
+
): () => void {
|
|
85
|
+
const original = owner[key]
|
|
86
|
+
const patched = ((...args: never[]) => wrapper(original, ...args)) as T[K]
|
|
87
|
+
owner[key] = patched
|
|
88
|
+
return () => {
|
|
89
|
+
if (owner[key] === patched) owner[key] = original
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 文件打开器注册表与自动选择。
|
|
3
|
+
* @author ddj 2026年08月24号
|
|
4
|
+
*/
|
|
5
|
+
import { SIDEBAR_PLUGIN } from './compat.js'
|
|
6
|
+
|
|
7
|
+
export const AUTO_OPEN_TOOL = 'auto'
|
|
8
|
+
export const SYSTEM_OPEN_TOOL = 'system'
|
|
9
|
+
export const VSCODE_OPEN_TOOL = 'dsh-vscode-mode'
|
|
10
|
+
export const SIDEBAR_OPEN_TOOL = SIDEBAR_PLUGIN
|
|
11
|
+
|
|
12
|
+
export interface FileOpenContext {
|
|
13
|
+
sessionId?: string
|
|
14
|
+
cwd?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface FileOpener {
|
|
18
|
+
id: string
|
|
19
|
+
label: string
|
|
20
|
+
description?: string
|
|
21
|
+
priority?: number
|
|
22
|
+
isAvailable?: () => boolean
|
|
23
|
+
open: (path: string, context: FileOpenContext) => void | Promise<void>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FileOpenerRegistry {
|
|
27
|
+
register(opener: FileOpener): () => void
|
|
28
|
+
registerFileOpener(opener: FileOpener): () => void
|
|
29
|
+
list(): readonly FileOpener[]
|
|
30
|
+
subscribe(listener: () => void): () => void
|
|
31
|
+
get(id: string): FileOpener | undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 写入外部打开器并触发注册表通知。
|
|
36
|
+
* @author ddj 2026年08月24号
|
|
37
|
+
* @param entries 打开器存储
|
|
38
|
+
* @param notify 变更通知
|
|
39
|
+
* @param opener 打开器
|
|
40
|
+
*/
|
|
41
|
+
function registryRegister(entries: Map<string, FileOpener>, notify: () => void, opener: FileOpener): void {
|
|
42
|
+
if (!opener.id || typeof opener.open !== 'function') throw new TypeError('文件打开器必须提供 id 和 open')
|
|
43
|
+
entries.set(opener.id, opener)
|
|
44
|
+
notify()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 创建生命周期独立的文件打开器注册表。
|
|
49
|
+
* @author ddj 2026年08月24号
|
|
50
|
+
* @returns 文件打开器注册表
|
|
51
|
+
*/
|
|
52
|
+
export function createFileOpenerRegistry(): FileOpenerRegistry {
|
|
53
|
+
const entries = new Map<string, FileOpener>()
|
|
54
|
+
const listeners = new Set<() => void>()
|
|
55
|
+
const notify = (): void => {
|
|
56
|
+
for (const listener of listeners) listener()
|
|
57
|
+
}
|
|
58
|
+
const list = (): readonly FileOpener[] => [...entries.values()].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
|
|
59
|
+
return {
|
|
60
|
+
register(opener: FileOpener): () => void {
|
|
61
|
+
registryRegister(entries, notify, opener)
|
|
62
|
+
return () => {
|
|
63
|
+
if (entries.get(opener.id) !== opener) return
|
|
64
|
+
entries.delete(opener.id)
|
|
65
|
+
notify()
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
registerFileOpener(opener: FileOpener): () => void {
|
|
69
|
+
registryRegister(entries, notify, opener)
|
|
70
|
+
return () => {
|
|
71
|
+
if (entries.get(opener.id) !== opener) return
|
|
72
|
+
entries.delete(opener.id)
|
|
73
|
+
notify()
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
list,
|
|
77
|
+
subscribe(listener: () => void): () => void {
|
|
78
|
+
listeners.add(listener)
|
|
79
|
+
return () => listeners.delete(listener)
|
|
80
|
+
},
|
|
81
|
+
get: (id: string) => entries.get(id),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 检查打开器当前是否可用。
|
|
87
|
+
* @author ddj 2026年08月24号
|
|
88
|
+
* @param opener 待检查打开器
|
|
89
|
+
* @returns 是否可用
|
|
90
|
+
*/
|
|
91
|
+
export function isAvailable(opener: FileOpener): boolean {
|
|
92
|
+
try {
|
|
93
|
+
return opener.isAvailable ? opener.isAvailable() : true
|
|
94
|
+
} catch {
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 返回当前可展示的文件打开器。
|
|
101
|
+
* @author ddj 2026年08月24号
|
|
102
|
+
* @param registry 文件打开器注册表
|
|
103
|
+
* @returns 可用打开器列表
|
|
104
|
+
*/
|
|
105
|
+
export function availableOpeners(registry: FileOpenerRegistry): FileOpener[] {
|
|
106
|
+
return registry.list().filter(isAvailable)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 按设置选择打开器,不可用时回退自动优先级。
|
|
111
|
+
* @author ddj 2026年08月24号
|
|
112
|
+
* @param registry 文件打开器注册表
|
|
113
|
+
* @param selected 用户选择的打开器 id
|
|
114
|
+
* @returns 选中的打开器
|
|
115
|
+
*/
|
|
116
|
+
export function selectOpener(
|
|
117
|
+
registry: FileOpenerRegistry,
|
|
118
|
+
selected: string,
|
|
119
|
+
): FileOpener | undefined {
|
|
120
|
+
if (selected !== AUTO_OPEN_TOOL) {
|
|
121
|
+
const requested = registry.get(selected)
|
|
122
|
+
if (requested && isAvailable(requested)) return requested
|
|
123
|
+
}
|
|
124
|
+
return availableOpeners(registry)[0]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 结构化探测 dsh-better-sidebar 的文件打开能力。
|
|
129
|
+
* @author ddj 2026年08月24号
|
|
130
|
+
* @param ctx 客户端服务上下文
|
|
131
|
+
* @returns 侧边栏打开器或 undefined
|
|
132
|
+
*/
|
|
133
|
+
export function scanSidebar(ctx: { get: (name: string) => unknown }): FileOpener | undefined {
|
|
134
|
+
try {
|
|
135
|
+
const service = ctx.get('betterSidebar') as {
|
|
136
|
+
openFile?: (scope: { sessionId: string; cwd?: string }, path: string, title?: string) => void
|
|
137
|
+
isTabEnabled?: (id: string) => boolean
|
|
138
|
+
features?: readonly string[]
|
|
139
|
+
getSnapshot?: () => { state?: unknown }
|
|
140
|
+
} | undefined
|
|
141
|
+
if (!service || typeof service.openFile !== 'function') return undefined
|
|
142
|
+
const advertised = service.features?.includes('openFile') ?? true
|
|
143
|
+
if (!advertised) return undefined
|
|
144
|
+
const canOpen = typeof service.isTabEnabled !== 'function' || service.isTabEnabled('editor')
|
|
145
|
+
if (!canOpen) return undefined
|
|
146
|
+
return {
|
|
147
|
+
id: SIDEBAR_OPEN_TOOL,
|
|
148
|
+
label: 'dsh-better-sidebar',
|
|
149
|
+
description: '在 dsh-web-ui-all 侧边栏编辑器中打开',
|
|
150
|
+
priority: 80,
|
|
151
|
+
isAvailable: () => typeof service.openFile === 'function'
|
|
152
|
+
&& (service.features?.includes('openFile') ?? true)
|
|
153
|
+
&& (typeof service.isTabEnabled !== 'function' || service.isTabEnabled('editor')),
|
|
154
|
+
open: (path, context) => {
|
|
155
|
+
if (!context.sessionId) throw new Error('当前没有活动会话')
|
|
156
|
+
service.openFile!({ sessionId: context.sessionId, cwd: context.cwd }, path, baseName(path))
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
return undefined
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 从路径提取文件名。
|
|
166
|
+
* @author ddj 2026年08月24号
|
|
167
|
+
* @param path 文件路径
|
|
168
|
+
* @returns 文件名
|
|
169
|
+
*/
|
|
170
|
+
export function baseName(path: string): string {
|
|
171
|
+
const normalized = path.replace(/\\/g, '/')
|
|
172
|
+
return normalized.slice(normalized.lastIndexOf('/') + 1) || path
|
|
173
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -16,8 +16,15 @@ 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 } from './settingsContext.js'
|
|
24
|
+
import { SIDEBAR_PLUGIN, pickSettingsBinder, registerSlotSafely } from './compat.js'
|
|
25
|
+
import type { CompatAdapter } from '../shared/compat.js'
|
|
19
26
|
|
|
20
|
-
export const inject = ['slots', 'timer', 'locale', 'connection', 'remote']
|
|
27
|
+
export const inject = ['slots', 'timer', 'locale', 'connection', 'remote', 'workspaces', 'sessions', 'settingsScope', 'webUiSettings']
|
|
21
28
|
|
|
22
29
|
/**
|
|
23
30
|
* 装配客户端:注册中央编辑区视图与 header 差异角标。
|
|
@@ -27,27 +34,75 @@ export const inject = ['slots', 'timer', 'locale', 'connection', 'remote']
|
|
|
27
34
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
35
|
export function apply(ctx: any): void {
|
|
29
36
|
const schedule = (fn: () => void, ms: number) => ctx.timeout(fn, ms)
|
|
37
|
+
const registry: FileOpenerRegistry = createFileOpenerRegistry()
|
|
38
|
+
const workspaces = ctx.get('workspaces')
|
|
39
|
+
const sessions = ctx.get('sessions')
|
|
40
|
+
const originalOpenPath = workspaces?.openPath
|
|
41
|
+
const binder = pickSettingsBinder(ctx)
|
|
42
|
+
const settings = binder.scope
|
|
43
|
+
let selected = autoValue('auto')
|
|
44
|
+
|
|
45
|
+
/** 客户端侧兼容摘要(与 host 报告合并展示;惰性求值保证 HMR 后仍新鲜)。 */
|
|
46
|
+
const compatSummary = (): CompatAdapter[] => [
|
|
47
|
+
{ name: '设置桥', active: settings !== undefined, note: settings !== undefined ? '使用 ' + binder.service + ' 桥' : '未绑定设置服务(fileOpenTool 持久化不可用)' },
|
|
48
|
+
{ name: '文件打开路由(workspaces.openPath)', active: Boolean(workspaces?.openPath), note: 'vscode 打开器优先,失败回退系统打开' },
|
|
49
|
+
{ name: '侧边栏打开器(' + SIDEBAR_PLUGIN + ')', active: registry.get(SIDEBAR_PLUGIN) !== undefined, note: registry.get(SIDEBAR_PLUGIN) !== undefined ? '已注册(优先级 80)' : '未检测到侧边栏打开能力' },
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
ctx.provide('fileOpeners', registry)
|
|
53
|
+
ctx.effect(() => registry.register(vscodeOpener()), 'vscode-mode: file opener')
|
|
54
|
+
ctx.effect(() => registry.register({
|
|
55
|
+
id: 'system', label: '系统默认应用', priority: 0,
|
|
56
|
+
open: (path: string) => originalOpenPath.call(workspaces, path),
|
|
57
|
+
}), 'vscode-mode: system file opener')
|
|
58
|
+
ctx.effect(() => {
|
|
59
|
+
const sidebar = scanSidebar(ctx)
|
|
60
|
+
return sidebar ? registry.register(sidebar) : undefined
|
|
61
|
+
}, 'vscode-mode: sidebar file opener')
|
|
62
|
+
ctx.effect(() => {
|
|
63
|
+
if (!settings) return undefined
|
|
64
|
+
const sync = (): void => {
|
|
65
|
+
const snapshot = settings.getSnapshot()
|
|
66
|
+
if (snapshot.status === 'loading') return
|
|
67
|
+
selected = autoValue(snapshot.value?.fileOpenTool)
|
|
68
|
+
window.dispatchEvent(new CustomEvent('edrv:file-open-tool-change', { detail: { value: selected } }))
|
|
69
|
+
}
|
|
70
|
+
sync()
|
|
71
|
+
return settings.subscribe(sync)
|
|
72
|
+
}, 'vscode-mode: file opener setting sync')
|
|
73
|
+
if (workspaces?.openPath) {
|
|
74
|
+
ctx.effect(() => installOpenPathRouter({
|
|
75
|
+
workspaces,
|
|
76
|
+
registry,
|
|
77
|
+
selected: () => selected,
|
|
78
|
+
context: () => {
|
|
79
|
+
const current = sessions?.list?.getSnapshot?.()
|
|
80
|
+
const sessionId = current?.current
|
|
81
|
+
const summary = sessionId ? current?.byId?.[sessionId] : undefined
|
|
82
|
+
return { sessionId, cwd: summary?.cwd } as FileOpenContext
|
|
83
|
+
},
|
|
84
|
+
logger: (message: string) => console.warn('[dsh-vscode-mode] ' + message),
|
|
85
|
+
}), 'vscode-mode: file link routing')
|
|
86
|
+
}
|
|
30
87
|
|
|
31
88
|
// 中央「文件编辑」页签:类 VSCode 编辑器(顶部=文件页签+搜索框,差异 UI=文件底部圆角悬浮框)
|
|
32
|
-
ctx
|
|
89
|
+
registerSlotSafely(ctx, {
|
|
33
90
|
name: 'conversation.view',
|
|
34
91
|
id: 'edrv-editor',
|
|
35
92
|
order: 5,
|
|
36
93
|
label: '文件编辑',
|
|
37
94
|
inject: (sessionId: string) => ({ sessionId }),
|
|
38
|
-
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { schedule })))
|
|
95
|
+
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { schedule })))
|
|
39
96
|
|
|
40
97
|
// header 差异角标:仅当前工作区存在差异时渲染
|
|
41
|
-
ctx
|
|
42
|
-
|
|
43
|
-
(props: unknown) => React.createElement(DiffBadge, Object.assign({}, props)),
|
|
44
|
-
))
|
|
98
|
+
registerSlotSafely(ctx, { name: 'conversation.session.header.utilities', id: 'edrv-diff-badge', order: 90, label: '差异' },
|
|
99
|
+
(props: unknown) => React.createElement(DiffBadge, Object.assign({}, props)))
|
|
45
100
|
|
|
46
101
|
// 设置页中的 VSCodeMode 专属页签,MCP 管理作为该页签的内部子 Tab。
|
|
47
|
-
ctx
|
|
102
|
+
registerSlotSafely(ctx, {
|
|
48
103
|
name: 'settings.section',
|
|
49
104
|
id: 'vscode-mode',
|
|
50
105
|
order: 30,
|
|
51
106
|
label: 'VSCodeMode',
|
|
52
|
-
}, () => React.createElement(McpSettings)))
|
|
107
|
+
}, () => React.createElement(SettingsContext.Provider, { value: settings }, React.createElement(McpSettings, { openerRegistry: registry, compatSummary })))
|
|
53
108
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — DSH 对话文件链接的统一打开路由。
|
|
3
|
+
* openPath 补丁经 compat.patchMethod 安装(链式恢复,多插件补丁互不踩踏)。
|
|
4
|
+
* 作者 ddj 2026年08月24号
|
|
5
|
+
*/
|
|
6
|
+
import { patchMethod } from './compat.js'
|
|
7
|
+
import { openEditorView } from './events.js'
|
|
8
|
+
import { AUTO_OPEN_TOOL, VSCODE_OPEN_TOOL, type FileOpenContext, type FileOpenerRegistry, isAvailable, selectOpener } from './fileOpeners.js'
|
|
9
|
+
|
|
10
|
+
export interface OpenPathRouterOptions {
|
|
11
|
+
workspaces: { openPath: (path: string) => Promise<void> }
|
|
12
|
+
registry: FileOpenerRegistry
|
|
13
|
+
selected: () => string
|
|
14
|
+
context: () => FileOpenContext
|
|
15
|
+
logger?: (message: string) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 安装可卸载的 workspaces.openPath 路由。
|
|
20
|
+
* @author ddj 2026年08月24号
|
|
21
|
+
* @param options 路由依赖
|
|
22
|
+
* @returns 恢复原方法的 disposer
|
|
23
|
+
*/
|
|
24
|
+
export function installOpenPathRouter(options: OpenPathRouterOptions): () => void {
|
|
25
|
+
const open = async (original: (path: string) => Promise<void>, path: string): Promise<void> => {
|
|
26
|
+
const context = options.context()
|
|
27
|
+
const opener = selectOpener(options.registry, options.selected())
|
|
28
|
+
if (!opener || !isAvailable(opener)) return original.call(options.workspaces, path)
|
|
29
|
+
try {
|
|
30
|
+
await opener.open(path, context)
|
|
31
|
+
} catch (error) {
|
|
32
|
+
options.logger?.('文件打开器 ' + opener.id + ' 失败,回退系统打开: ' + String(error))
|
|
33
|
+
if (opener.id !== 'system') await original.call(options.workspaces, path)
|
|
34
|
+
else throw error
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return patchMethod(options.workspaces, 'openPath', open as never)
|
|
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,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 设置 scope 的跨组件上下文。
|
|
3
|
+
* 绑定逻辑(webUiSettings 兼容桥 → 官方 settingsScope 降级)统一由 compat 层提供。
|
|
4
|
+
* 作者 ddj 2026年08月24号
|
|
5
|
+
*/
|
|
6
|
+
import React from 'react'
|
|
7
|
+
import { pickSettingsBinder, type SettingsScopeLike } from './compat.js'
|
|
8
|
+
|
|
9
|
+
export type { SettingsScopeLike }
|
|
10
|
+
|
|
11
|
+
export const SettingsContext = React.createContext<SettingsScopeLike | undefined>(undefined)
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 从 Cordis 服务绑定 VSCodeMode 设置 scope(兼容桥优先,官方次之)。
|
|
15
|
+
* @author ddj 2026年08月24号
|
|
16
|
+
* @param ctx 客户端服务上下文
|
|
17
|
+
* @returns 设置 scope 或 undefined
|
|
18
|
+
*/
|
|
19
|
+
export function getSettingsScope(ctx: { get: (name: string) => unknown }): SettingsScopeLike | undefined {
|
|
20
|
+
return pickSettingsBinder(ctx).scope
|
|
21
|
+
}
|
|
@@ -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)}
|
|
@@ -85,3 +93,11 @@
|
|
|
85
93
|
.vsm-mcp-dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:20px}
|
|
86
94
|
.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
95
|
.vsm-mcp-dialog-actions button:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
|
|
96
|
+
|
|
97
|
+
/* 兼容性小节 */
|
|
98
|
+
.vsm-compat-h3{margin:16px 0 2px;font-size:14px;font-weight:600;color:var(--dsw-alias-label-secondary,#c8c8c8)}
|
|
99
|
+
.vsm-compat-item{display:flex;align-items:baseline;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px solid var(--dsw-alias-border-l1,#333);font-size:13px;color:var(--dsw-alias-label-primary,#eee)}
|
|
100
|
+
.vsm-compat-item.ok .vsm-compat-name::before{content:"● ";color:var(--dsw-alias-state-success-primary,#38bd75)}
|
|
101
|
+
.vsm-compat-item.warn .vsm-compat-name::before{content:"● ";color:var(--dsw-alias-state-warn-primary,#e5b93f)}
|
|
102
|
+
.vsm-compat-name{font-weight:500;min-width:0}
|
|
103
|
+
.vsm-compat-note{color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:12px;text-align:right;min-width:0}
|
|
@@ -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
|
}
|