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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.14",
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 { 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,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
+ }
@@ -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.slots.inject('conversation.view', () => ctx.slots.register({
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.slots.inject('conversation.session.header.utilities', () => ctx.slots.register(
42
- { name: 'conversation.session.header.utilities', id: 'edrv-diff-badge', order: 90, label: '差异' },
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.slots.inject('settings.section', () => ctx.slots.register({
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) 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
  }