dsh-vscode-mode 0.1.62 → 0.2.0

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,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.62",
4
- "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
3
+ "version": "0.2.0",
4
+ "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
7
7
  "dsh-plugin",
@@ -0,0 +1,83 @@
1
+ /**
2
+ * dsh-vscode-mode client — 指令执行桥(装配层)。
3
+ * 职责三件:① 把内置指令目录注册进注册表;② 为「桥接派发」类指令挂窗口 capture 键位监听
4
+ * (EditorView 原生监听的 8 条不进这里,避免双执行);③ 注入命令栏执行器。
5
+ * 编辑器动作本身由 EditorView / QuickOpen 监听 `edrv.command.<action>` 窗口事件完成——桥不持有
6
+ * React 状态,因此指令系统可在编辑器挂载前装配、卸载后仍安全。
7
+ * 作者 ddj 2026年09月10号
8
+ */
9
+ import { BRIDGE_COMMANDS, EDITOR_COMMANDS, dispatchedCommands, showCommandsDef } from './ui/commandCatalog.js'
10
+ import { createCommandRegistry } from './commandRegistry.js'
11
+ import type { CommandRegistry } from './commandRegistry.js'
12
+ import { bindingsOf, matchEvent } from './keybindings.js'
13
+ import { log } from './log.js'
14
+ import { openCommandPalette, setPaletteRunner, setRegistryRef } from './commandPaletteStore.js'
15
+
16
+ const bridgeLog = log.child('commands')
17
+
18
+ /** 桥接装配选项(事件目标可注入,便于单测)。 */
19
+ export interface CommandBridgeOptions {
20
+ /** 键盘事件目标(缺省 window;无 window 的纯 Node 环境自动跳过监听)。 */
21
+ target?: Pick<EventTarget, 'addEventListener' | 'removeEventListener'>
22
+ }
23
+
24
+ /** 指令执行桥句柄。 */
25
+ export interface CommandBridge {
26
+ /** 指令注册表(第三方可继续 register)。 */
27
+ registry: CommandRegistry
28
+ /** 卸载全部内置指令并解除键位监听(幂等)。 */
29
+ dispose(): void
30
+ }
31
+
32
+ /** 解析事件目标(缺省 window;无 window 返回 null)。 */
33
+ function resolveTarget(options: CommandBridgeOptions): CommandBridgeOptions['target'] | null {
34
+ if (options.target) return options.target
35
+ if (typeof window === 'undefined') return null
36
+ return window
37
+ }
38
+
39
+ /**
40
+ * 创建指令执行桥:注册内置指令 + 桥接类指令的全局键位监听。
41
+ * @author ddj 2026年09月10号
42
+ * @param options 装配选项(事件目标注入)
43
+ * @returns 指令执行桥句柄
44
+ */
45
+ export function createCommandBridge(options: CommandBridgeOptions = {}): CommandBridge {
46
+ const registry = createCommandRegistry()
47
+ const disposers: Array<() => void> = []
48
+ const palette = showCommandsDef(() => openCommandPalette('command'))
49
+ for (const command of [...EDITOR_COMMANDS, ...BRIDGE_COMMANDS]) {
50
+ disposers.push(registry.register(command))
51
+ }
52
+ disposers.push(registry.register(palette))
53
+ setPaletteRunner((id) => registry.run(id))
54
+ // 命令栏候选来源:模块引用直传(旧实现依赖 window.dsh,DSH 无该命名空间 → 恒空表)
55
+ setRegistryRef(registry)
56
+
57
+ // 全局键位派发集合:桥接类指令 + 命令栏自身(EditorView/QuickOpen 原生监听的 8 条不在内,避免双执行)
58
+ const dispatched = dispatchedCommands(palette)
59
+ const onKey = (event: KeyboardEvent): void => {
60
+ for (const command of dispatched) {
61
+ if (!matchEvent(event, bindingsOf(command.id))) continue
62
+ event.preventDefault()
63
+ event.stopPropagation()
64
+ registry.run(command.id)
65
+ return
66
+ }
67
+ }
68
+ const target = resolveTarget(options)
69
+ if (target) target.addEventListener('keydown', onKey as EventListener, true)
70
+
71
+ const dispose = (): void => {
72
+ if (target) target.removeEventListener('keydown', onKey as EventListener, true)
73
+ while (disposers.length) {
74
+ const release = disposers.pop()
75
+ if (release) release()
76
+ }
77
+ setPaletteRunner(null)
78
+ setRegistryRef(null)
79
+ }
80
+ bridgeLog.info('指令注册表已装配(' + registry.list().length + ' 条指令,桥接键位 '
81
+ + dispatched.length + ' 条)')
82
+ return { registry, dispose }
83
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * dsh-vscode-mode client — 指令系统的全局挂点名约定。
3
+ * 单独成模块:装配层(client/index.ts)与读取方(命令栏/第三方)都要用,
4
+ * 放这里可避免 CommandPalette ↔ index 的循环依赖。
5
+ *
6
+ * ⚠️ 必须挂在 window 自身(双下划线 + edrv 前缀,与既有 __edrvExtPoll 同风格)。
7
+ * 旧实现写 window.dsh.edrvCommands——DSH 从不创建 window.dsh 命名空间(全仓库无赋值),
8
+ * 该路径恒为死代码并导致命令栏读到空表;不要再改回气泡式挂点。
9
+ * 作者 ddj 2026年09月10号
10
+ */
11
+
12
+ /** 指令注册表的全局挂点名:window[REGISTRY_GLOBAL](第三方注册与调试读取;命令栏首选模块引用)。 */
13
+ export const REGISTRY_GLOBAL = '__edrvCommands__'
@@ -0,0 +1,165 @@
1
+ /**
2
+ * dsh-vscode-mode client — 命令栏开关状态与单实例宿主认领。
3
+ * 命令栏浮层走 createPortal 渲染到 body,但组件必须挂在插件自己的 React 树里:
4
+ * 编辑区有三种形态(官方右侧 Sidebar / better-sidebar / 中央页签),会话切换时旧树先卸载、
5
+ * 新树后挂载,可能出现两个宿主——claimPaletteHost 保证任一时刻只有一个宿主真正渲染浮层。
6
+ * 运行体由 commandBridge 经 setPaletteRunner 注入(避免 store ↔ 注册表循环依赖);
7
+ * 注册表本体经 setRegistryRef 存入模块引用,命令栏据此读取候选——不依赖任何 window 全局
8
+ * (旧实现写 window.dsh.edrvCommands,而 DSH 从不创建 window.dsh,导致命令栏恒为空表)。
9
+ * 作者 ddj 2026年09月10号
10
+ */
11
+ import type { CommandRegistry } from './commandRegistry.js'
12
+
13
+ /** 命令栏上次的唤起来源(诊断用)。 */
14
+ let openReason = ''
15
+
16
+ /** 装配期存入的指令注册表(命令栏读取候选的唯一真源)。 */
17
+ let registryTable: CommandRegistry | null = null
18
+
19
+ /** 命令栏是否展开。 */
20
+ let opened = false
21
+
22
+ /** 当前真正渲染浮层的宿主令牌(null = 无宿主)。 */
23
+ let hostToken: object | null = null
24
+
25
+ /** 注入的命令执行器(commandBridge 装配时写入)。 */
26
+ let runner: ((id: string) => boolean) | null = null
27
+
28
+ /** 打开命令栏前的焦点元素(关闭后归还焦点,键盘操作不中断)。 */
29
+ let focusBack: HTMLElement | null = null
30
+
31
+ const listeners = new Set<() => void>()
32
+
33
+ /** 通知全部订阅者(回调异常不影响其他订阅)。 */
34
+ function notifyPalette(): void {
35
+ for (const listener of listeners) {
36
+ try {
37
+ listener()
38
+ } catch {
39
+ /* 订阅回调异常忽略 */
40
+ }
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 注入命令执行器(命令栏执行选中命令时调用)。
46
+ * @author ddj 2026年09月10号
47
+ * @param next 执行器(返回是否执行成功)
48
+ */
49
+ export function setPaletteRunner(next: ((id: string) => boolean) | null): void {
50
+ runner = next
51
+ }
52
+
53
+ /**
54
+ * 执行命令栏选中的命令。
55
+ * @author ddj 2026年09月10号
56
+ * @param id 命令 id
57
+ * @returns 是否执行成功(执行器未注入/命令不可用/抛异常均为 false)
58
+ */
59
+ export function runPaletteCommand(id: string): boolean {
60
+ if (!runner) return false
61
+ return runner(id) === true
62
+ }
63
+
64
+ /**
65
+ * 存入指令注册表(commandBridge 装配期调用;命令栏据此读取候选)。
66
+ * @author ddj 2026年09月10号
67
+ * @param next 指令注册表(null = 卸载)
68
+ */
69
+ export function setRegistryRef(next: CommandRegistry | null): void {
70
+ registryTable = next
71
+ }
72
+
73
+ /**
74
+ * 读取指令注册表(未装配返回 null;浮层据此回退全局/空表)。
75
+ * @author ddj 2026年09月10号
76
+ * @returns 指令注册表或 null
77
+ */
78
+ export function registryRef(): CommandRegistry | null {
79
+ return registryTable
80
+ }
81
+
82
+ /**
83
+ * 订阅命令栏开关变化。
84
+ * @author ddj 2026年09月10号
85
+ * @param listener 变化回调
86
+ * @returns 取消订阅函数
87
+ */
88
+ export function subscribePalette(listener: () => void): () => void {
89
+ listeners.add(listener)
90
+ return () => listeners.delete(listener)
91
+ }
92
+
93
+ /**
94
+ * 命令栏是否展开(useSyncExternalStore 快照)。
95
+ * @author ddj 2026年09月10号
96
+ * @returns 是否展开
97
+ */
98
+ export function isPaletteOpen(): boolean {
99
+ return opened
100
+ }
101
+
102
+ /** 记录打开前的焦点元素(只在真正需要归还时记录)。 */
103
+ function rememberFocus(): void {
104
+ if (typeof document === 'undefined') return
105
+ const active = document.activeElement
106
+ focusBack = active instanceof HTMLElement ? active : null
107
+ }
108
+
109
+ /**
110
+ * 打开命令栏(已打开时为空操作;记录当前焦点供关闭后归还)。
111
+ * @author ddj 2026年09月10号
112
+ * @param reason 唤起来源(键盘/命令/外部 API)
113
+ */
114
+ export function openCommandPalette(reason = 'keybinding'): void {
115
+ if (opened) return
116
+ rememberFocus()
117
+ openReason = reason
118
+ opened = true
119
+ notifyPalette()
120
+ }
121
+
122
+ /**
123
+ * 关闭命令栏并归还焦点。
124
+ * @author ddj 2026年09月10号
125
+ */
126
+ export function closeCommandPalette(): void {
127
+ if (!opened) return
128
+ opened = false
129
+ openReason = ''
130
+ notifyPalette()
131
+ const target = focusBack
132
+ focusBack = null
133
+ if (!target || typeof target.focus !== 'function') return
134
+ try {
135
+ target.focus()
136
+ } catch {
137
+ /* 目标已卸载时忽略 */
138
+ }
139
+ }
140
+
141
+ /** 上次唤起来源(诊断/测试读取)。 */
142
+ export function paletteReason(): string {
143
+ return openReason
144
+ }
145
+
146
+ /**
147
+ * 认领命令栏宿主(返回 true 表示由本宿主渲染浮层)。
148
+ * @author ddj 2026年09月10号
149
+ * @returns 是否为当前宿主
150
+ */
151
+ export function claimPaletteHost(): boolean {
152
+ if (hostToken !== null) return false
153
+ hostToken = {}
154
+ return true
155
+ }
156
+
157
+ /**
158
+ * 释放宿主认领(仅持有者可释放,卸载顺序错乱不会顶掉新宿主)。
159
+ * @author ddj 2026年09月10号
160
+ * @param token 认领令牌(null 表示本宿主未认领)
161
+ */
162
+ export function releasePaletteHost(token: object | null): void {
163
+ if (token === null || hostToken !== token) return
164
+ hostToken = null
165
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * dsh-vscode-mode client — 指令注册表(命令栏与快捷键系统的唯一数据源)。
3
+ * 镜像 sidebar/registry.ts 的注册表模式:register 返回注销器、list/subscribe 可订阅;
4
+ * 额外提供 run(id):先做可用性判定再执行,运行体异常一律捕获上报,绝不让命令抛出到事件循环。
5
+ * 重复 id 按「后注册者生效」覆盖,旧注销器带身份校验(不会误删新实例,见 releaseRegistry)。
6
+ * 作者 ddj 2026年09月10号
7
+ */
8
+ import { filterCommands } from './commandSearch.js'
9
+ import { log } from './log.js'
10
+ import type { CommandDef } from './ui/commandCatalog.js'
11
+
12
+ /** 指令注册表(对插件内与第三方一致)。 */
13
+ export interface CommandRegistry {
14
+ /** 注册命令;返回注销函数(幂等,重复调用无效)。 */
15
+ register(command: CommandDef): () => void
16
+ /** 命令是否存在。 */
17
+ has(id: string): boolean
18
+ /** 读取命令定义。 */
19
+ get(id: string): CommandDef | undefined
20
+ /** 全部命令(按目录序稳定排序)。 */
21
+ list(): CommandDef[]
22
+ /** 可用命令(available 缺省视为可用)。 */
23
+ available(): CommandDef[]
24
+ /** 命令栏过滤(先按可用性过滤,再按相关度排序)。 */
25
+ match(query: string): CommandDef[]
26
+ /** 执行命令;不可用/未注册/抛异常统一返回 false(不抛出)。 */
27
+ run(id: string): boolean
28
+ /** 订阅注册表变化。 */
29
+ subscribe(listener: () => void): () => void
30
+ }
31
+
32
+ const registryLog = log.child('commands')
33
+
34
+ /** 校验命令定义(缺 id/label/run 抛 TypeError,早失败优于静默无效)。 */
35
+ function assertCommand(command: CommandDef): void {
36
+ if (!command || typeof command.id !== 'string' || !command.id) throw new TypeError('命令必须提供 id')
37
+ if (typeof command.label !== 'string' || !command.label) throw new TypeError('命令必须提供 label:' + command.id)
38
+ if (typeof command.run !== 'function') throw new TypeError('命令必须提供 run:' + command.id)
39
+ }
40
+
41
+ /** 目录序(order 缺省 100;非有限值同样按 100)。 */
42
+ function orderOf(command: CommandDef): number {
43
+ return typeof command.order === 'number' && Number.isFinite(command.order) ? command.order : 100
44
+ }
45
+
46
+ /** 命令是否可用(available 缺省视为可用;判定异常按不可用处理)。 */
47
+ function isAvailable(command: CommandDef): boolean {
48
+ if (typeof command.available !== 'function') return true
49
+ try {
50
+ return command.available() === true
51
+ } catch (error) {
52
+ registryLog.warn('可用性判定异常(按不可用处理):' + command.id + ' · ' + String(error))
53
+ return false
54
+ }
55
+ }
56
+
57
+ /**
58
+ * 注销命令(仅当表中仍是本次注册的定义时移除,避免旧实例误删新实例)。
59
+ * @author ddj 2026年09月10号
60
+ * @param entries 命令表
61
+ * @param notify 变化通知
62
+ * @param command 注册时的定义
63
+ * @returns 注销函数
64
+ */
65
+ function releaseRegistry(entries: Map<string, CommandDef>, notify: () => void, command: CommandDef): () => void {
66
+ let released = false
67
+ return () => {
68
+ if (released) return
69
+ released = true
70
+ if (entries.get(command.id) !== command) return
71
+ entries.delete(command.id)
72
+ notify()
73
+ }
74
+ }
75
+
76
+ /**
77
+ * 执行已注册命令(可用性判定 → 运行体 → 异常上报)。
78
+ * @author ddj 2026年09月10号
79
+ * @param command 命令定义
80
+ * @returns 是否真正执行
81
+ */
82
+ function runCommand(command: CommandDef): boolean {
83
+ if (!isAvailable(command)) {
84
+ registryLog.debug('命令不可用,已跳过:' + command.id)
85
+ return false
86
+ }
87
+ try {
88
+ command.run()
89
+ registryLog.debug('命令已执行:' + command.id)
90
+ return true
91
+ } catch (error) {
92
+ registryLog.error('命令执行失败:' + command.id + ' · ' + String(error))
93
+ return false
94
+ }
95
+ }
96
+
97
+ /**
98
+ * 创建指令注册表。
99
+ * @author ddj 2026年09月10号
100
+ * @returns 指令注册表
101
+ */
102
+ export function createCommandRegistry(): CommandRegistry {
103
+ const entries = new Map<string, CommandDef>()
104
+ const listeners = new Set<() => void>()
105
+ const notify = (): void => {
106
+ for (const listener of listeners) {
107
+ try {
108
+ listener()
109
+ } catch (error) {
110
+ registryLog.warn('注册表订阅回调异常:' + String(error))
111
+ }
112
+ }
113
+ }
114
+ const list = (): CommandDef[] =>
115
+ [...entries.values()].sort((a, b) => orderOf(a) - orderOf(b))
116
+ const available = (): CommandDef[] => list().filter(isAvailable)
117
+ return {
118
+ register(command: CommandDef): () => void {
119
+ assertCommand(command)
120
+ const replaced = entries.get(command.id)
121
+ if (replaced !== undefined) registryLog.warn('命令 id 重复注册(后者生效):' + command.id)
122
+ entries.set(command.id, command)
123
+ notify()
124
+ return releaseRegistry(entries, notify, command)
125
+ },
126
+ has: (id) => entries.has(id),
127
+ get: (id) => entries.get(id),
128
+ list,
129
+ available,
130
+ match: (query) => filterCommands(available(), query),
131
+ run(id: string): boolean {
132
+ const command = entries.get(id)
133
+ if (!command) {
134
+ registryLog.warn('命令未注册:' + id)
135
+ return false
136
+ }
137
+ return runCommand(command)
138
+ },
139
+ subscribe(listener: () => void): () => void {
140
+ listeners.add(listener)
141
+ return () => listeners.delete(listener)
142
+ },
143
+ }
144
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * dsh-vscode-mode client — 命令栏筛选与排序(纯函数,可单测)。
3
+ * 匹配语义:查询与索引文本统一小写,空格分词,全部 token 需在 label/id/category
4
+ * 任一字段中以子串出现(中文子串匹配天然成立)。
5
+ * 排序语义:命中字段权重(label 0 < id 1 < category 2)+ 目录序(order 缺省 100)+ 注册序,
6
+ * 保证同一查询下结果稳定、越相关的越靠前。
7
+ * 作者 ddj 2026年09月10号
8
+ */
9
+ import type { CommandDef } from './ui/commandCatalog.js'
10
+
11
+ /** 排序用基础序(未声明 order 的命令排在已声明之后)。 */
12
+ const DEFAULT_ORDER = 100
13
+
14
+ /** 命中字段权重(数值越小越优先)。 */
15
+ const FIELD_WEIGHT: Record<string, number> = { label: 0, id: 1, category: 2 }
16
+
17
+ /** 命令栏候选行(命令定义 + 匹配权重)。 */
18
+ export interface CommandHit {
19
+ command: CommandDef
20
+ weight: number
21
+ }
22
+
23
+ /** 命中字段索引文本(小写)。 */
24
+ interface IndexedFields {
25
+ label: string
26
+ id: string
27
+ category: string
28
+ }
29
+
30
+ /** 小写化(非字符串按空串处理,容错外部脏数据)。 */
31
+ function lower(text: unknown): string {
32
+ return typeof text === 'string' ? text.toLocaleLowerCase('en-US') : ''
33
+ }
34
+
35
+ /** 建立一条命令的索引文本。 */
36
+ function indexOf(command: CommandDef): IndexedFields {
37
+ return { label: lower(command.label), id: lower(command.id), category: lower(command.category) }
38
+ }
39
+
40
+ /**
41
+ * 命令是否命中全部 token;命中时返回最优(最小)字段权重。
42
+ * @author ddj 2026年09月10号
43
+ * @param fields 命令索引文本
44
+ * @param tokens 小写查询 token
45
+ * @returns 字段权重;未命中返回 null
46
+ */
47
+ function hitWeight(fields: IndexedFields, tokens: readonly string[]): number | null {
48
+ let best: number | null = null
49
+ for (const token of tokens) {
50
+ let tokenWeight: number | null = null
51
+ for (const field of ['label', 'id', 'category'] as const) {
52
+ if (!fields[field].includes(token)) continue
53
+ const weight = FIELD_WEIGHT[field]
54
+ if (tokenWeight === null || weight < tokenWeight) tokenWeight = weight
55
+ if (weight === 0) break
56
+ }
57
+ if (tokenWeight === null) return null
58
+ if (best === null || tokenWeight < best) best = tokenWeight
59
+ }
60
+ return best
61
+ }
62
+
63
+ /** 目录序(order 缺省 100;非有限值同样按 100)。 */
64
+ function orderOf(command: CommandDef): number {
65
+ const order = command.order
66
+ return typeof order === 'number' && Number.isFinite(order) ? order : DEFAULT_ORDER
67
+ }
68
+
69
+ /**
70
+ * 过滤并按相关度排序命令(空查询返回全部,仅按目录序排序)。
71
+ * @author ddj 2026年09月10号
72
+ * @param commands 候选命令(调用方通常已按可用性过滤)
73
+ * @param query 用户输入
74
+ * @returns 命中命令数组
75
+ */
76
+ export function filterCommands(commands: readonly CommandDef[], query: string): CommandDef[] {
77
+ const tokens = lower(query).split(/\s+/).filter(Boolean)
78
+ const hits: CommandHit[] = []
79
+ for (const command of commands) {
80
+ const weight = tokens.length ? hitWeight(indexOf(command), tokens) : 0
81
+ if (weight !== null) hits.push({ command, weight })
82
+ }
83
+ return hits
84
+ .map((hit, index) => ({ hit, index }))
85
+ .sort((a, b) => a.hit.weight - b.hit.weight
86
+ || orderOf(a.hit.command) - orderOf(b.hit.command)
87
+ || a.index - b.index)
88
+ .map((entry) => entry.hit.command)
89
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * dsh-vscode-mode client — 编辑器模型可用性探测(模块级,不依赖 React)。
3
+ * 命令可用性判定(快捷键过滤 / 命令栏隐藏 / 执行前置检查)需要「当前是否有活动编辑器」,
4
+ * 而命令目录是模块级常量、拿不到 EditorView 实例,故统一读 DOM 标记:
5
+ * `[data-edrv-view]` 只在 EditorView 挂载期存在于文档中(浮层 portal 走同一根节点)。
6
+ * 作者 ddj 2026年09月10号
7
+ */
8
+
9
+ /** 编辑器根节点选择器(EditorView 三种形态共用)。 */
10
+ export const EDITOR_ROOT_SELECTOR = '[data-edrv-view]'
11
+
12
+ /**
13
+ * 当前文档是否挂载了编辑器视图(无 document 的运行环境返回 false)。
14
+ * @author ddj 2026年09月10号
15
+ * @returns 是否存在编辑器根节点
16
+ */
17
+ export function hasEditorView(): boolean {
18
+ if (typeof document === 'undefined') return false
19
+ return document.querySelector(EDITOR_ROOT_SELECTOR) !== null
20
+ }
21
+
22
+ /**
23
+ * 当前文档是否已有 Monaco 活动模型(编辑器打开着文件而非空态)。
24
+ * @author ddj 2026年09月10号
25
+ * @returns 是否存在活动模型
26
+ */
27
+ export function hasEditorModel(): boolean {
28
+ if (typeof document === 'undefined') return false
29
+ return document.querySelector('.monaco-editor textarea.inputarea') !== null
30
+ }
@@ -22,6 +22,7 @@ import { ConversationDiffDock } from './ui/ConversationDiffDock.js'
22
22
  import { McpSettings } from './ui/McpSettings.js'
23
23
  import { rpc } from './rpc.js'
24
24
  import { loadMonaco } from './monaco/loader.js'
25
+ import { observeScheme, schemeOfSnapshot } from './monaco/theme.js'
25
26
  import { createFileOpenerRegistry, scanSidebar, officialSidebarOpener, shouldClaimFiles, type FileOpenContext } from './fileOpeners.js'
26
27
  import type { FileOpenerRegistry } from './fileOpeners.js'
27
28
  import { installOpenPathRouter, vscodeOpener, autoValue } from './openPathRouter.js'
@@ -33,6 +34,7 @@ import { detectSidebarService, installSideEditor, setEnsureSideEditor, SIDEBAR_I
33
34
  import { detectOfficial, installOfficial, registerOfficialFileClaim, OFFICIAL_TAB_KIND, OFFICIAL_TAB_TITLE, isEditorTabActive, restoreEditorTab } from './officialSidebar.js'
34
35
  import { SideEditorTab } from './ui/SideEditorTab.js'
35
36
  import { OfficialSideTab } from './ui/OfficialSideTab.js'
37
+ import { createClaimRouter } from './ui/ClaimRouter.js'
36
38
  import { createAddToConversation } from './addToConversation.js'
37
39
  import { createSidebarPanelRegistry } from './sidebar/registry.js'
38
40
  import { createFilePanel } from './sidebar/panels/index.js'
@@ -44,6 +46,8 @@ import { createOutlinePanel } from './outline/index.js'
44
46
  import { createOutlineSourceRegistry, registerBuiltinOutlineSources } from './outline/sources.js'
45
47
  import { createLspOutlineSource } from './outline/lspSource.js'
46
48
  import { keybindingsApply } from './keybindings.js'
49
+ import { createCommandBridge } from './commandBridge.js'
50
+ import { REGISTRY_GLOBAL } from './commandGlobals.js'
47
51
  import { sidebarMinApply } from './sidebarMin.js'
48
52
  import { log } from './log.js'
49
53
  import { setupLsp, setSession } from './monaco/lsp/index.js'
@@ -54,6 +58,33 @@ import type { CompatAdapter } from '../shared/compat.js'
54
58
  // web boot 直接失败('1 entry did not activate')。兼容层用 ctx.get 运行时探测 + 降级,不靠 inject。
55
59
  export const inject = ['slots', 'timer', 'locale', 'connection', 'remote', 'workspaces', 'sessions', 'conversation', 'settingsScope']
56
60
 
61
+ /** 指令桥装配幂等标记(同一 document 重复 apply 只装配一次,避免重复键位监听)。 */
62
+ let commandsMounted = false
63
+
64
+ /**
65
+ * 装配指令注册表(指令桥 + 命令栏)。
66
+ * 指令桥不依赖已挂载编辑器(run 只派发窗口事件),故可在启动期装配;
67
+ * 命令栏浮层的宿主由 CommandPalette 自己在编辑区树内认领。
68
+ * @author ddj 2026年09月10号
69
+ * @param ctx 客户端根上下文
70
+ * @returns void
71
+ */
72
+ function setupCommands(ctx: any): void {
73
+ if (commandsMounted) return
74
+ commandsMounted = true
75
+ const bridge = createCommandBridge()
76
+ ctx.provide(REGISTRY_GLOBAL, bridge.registry)
77
+ // 无条件镜像到 window(DSH 不创建 window.dsh,旧条件式赋值是死代码 → 命令栏空表)
78
+ const host = window as unknown as Record<string, unknown>
79
+ host[REGISTRY_GLOBAL] = bridge.registry
80
+ ctx.effect(() => () => {
81
+ bridge.dispose()
82
+ commandsMounted = false
83
+ if (host[REGISTRY_GLOBAL] === bridge.registry) delete host[REGISTRY_GLOBAL]
84
+ }, 'vscode-mode: command registry')
85
+ log.info('指令系统已装配:' + bridge.registry.list().length + ' 条指令,Ctrl+Shift+P 打开命令栏')
86
+ }
87
+
57
88
  /**
58
89
  * 装配客户端:注册中央编辑区视图与 header 差异角标。
59
90
  * @author ddj 2026年08月20号
@@ -63,6 +94,9 @@ export const inject = ['slots', 'timer', 'locale', 'connection', 'remote', 'work
63
94
  export function apply(ctx: any): void {
64
95
  const schedule = (fn: () => void, ms: number) => ctx.timeout(fn, ms)
65
96
 
97
+ // 指令系统(命令栏 + 指令注册表)先装配:命令栏在被任何 React slot 渲染前即可唤起
98
+ setupCommands(ctx)
99
+
66
100
  const registry: FileOpenerRegistry = createFileOpenerRegistry()
67
101
  const workspaces = ctx.get('workspaces')
68
102
  const sessions = ctx.get('sessions')
@@ -148,7 +182,7 @@ export function apply(ctx: any): void {
148
182
  { name: '侧边栏打开器(' + SIDEBAR_PLUGIN + ')', active: registry.get(SIDEBAR_PLUGIN) !== undefined, note: registry.get(SIDEBAR_PLUGIN) !== undefined ? '已注册(优先级 80)' : '未检测到侧边栏打开能力' },
149
183
  { name: '侧边栏编辑区(官方 Sidebar)', active: officialService !== undefined, note: officialService !== undefined ? '编辑区=官方右侧 Sidebar Tab(对话+编辑同屏,DSH 0.1.5+)' : '官方侧边栏服务未探测到(DSH < 0.1.5-alpha.1 时属预期)' },
150
184
  { name: '侧边栏编辑区(' + SIDEBAR_PLUGIN + ',归档)', active: sideService !== undefined, note: sideService !== undefined ? '编辑区=侧边栏 Tab(旧版 DSH 回退形态)' : '未检测到;DSH ≥ 0.1.5 优先官方侧边栏,旧版可安装(' + SIDEBAR_INSTALL_CMD + ')' },
151
- { name: '文件链接官方认领(dsh-resource://file)', active: claimDisposer !== null, note: claimDisposer !== null ? '聊天文件链接由本插件编辑器接管(官方侧边栏内打开)' : '链接走官方查看器或旧版路由(未认领)' },
185
+ { name: '文件链接官方认领(dsh-resource://file)', active: claimDisposer !== null, note: claimDisposer !== null ? '聊天文件链接转发进单一编辑器页签(文件分页归编辑器自带页签栏)' : '链接走官方查看器或旧版路由(未认领)' },
152
186
  ]
153
187
 
154
188
  ctx.provide('fileOpeners', registry)
@@ -183,14 +217,16 @@ export function apply(ctx: any): void {
183
217
  const sidebar = scanSidebar(ctx)
184
218
  return sidebar ? registry.register(sidebar) : undefined
185
219
  }, 'vscode-mode: sidebar file opener')
186
- // 官方侧边栏正文组件装配(页类型与 file 认领两类 Tab 共用同一 EditorView 形态)
220
+ // 官方侧边栏正文组件装配(页类型正文;兼作 file 认领转发失败时的兜底正文)
187
221
  const officialRenderTab = (props: Record<string, unknown>) => React.createElement(OfficialSideTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions }))
188
- /** 官方 file 地址认领同步:自动/VSCodeMode 档认领(链接进本插件编辑器),其余交官方查看器。 */
222
+ /** 官方 file 地址认领同步:自动/VSCodeMode 档认领(转发进单一编辑器页签),其余交官方查看器。 */
189
223
  const syncFileClaim = (): void => {
190
224
  const official = officialService
191
225
  const want = official !== undefined && shouldClaimFiles(selected)
192
226
  if (want && claimDisposer === null && official) {
193
- const disposer = registerOfficialFileClaim({ tabs: official.tabs, slots: ctx.slots, renderTab: officialRenderTab })
227
+ // 认领正文只做转发:文件分页收归编辑器自带页签栏,官方侧栏不再按文件分裂编辑器实例
228
+ const renderTab = createClaimRouter({ service: official.service, schedule, fallback: officialRenderTab })
229
+ const disposer = registerOfficialFileClaim({ tabs: official.tabs, slots: ctx.slots, renderTab })
194
230
  if (disposer !== null) claimDisposer = ctx.effect(() => disposer, 'vscode-mode: official file claim')
195
231
  return
196
232
  }
@@ -265,6 +301,21 @@ export function apply(ctx: any): void {
265
301
  }
266
302
  retryRemoteOpen()
267
303
 
304
+ // 官方主题跟随(DSH 0.1.5+):ctx.theme 快照事件优先,其次明暗标记观察(observeScheme),
305
+ // 都不可用时 theme.ts 的 detectColorScheme 兜底;统一广播 edrv:theme-change 触发 Monaco 主题重刷。
306
+ const themeService = ctx.get('theme') as { getTheme?: () => unknown } | undefined
307
+ const emitTheme = (): void => {
308
+ const snapshot = typeof themeService?.getTheme === 'function' ? themeService.getTheme() : undefined
309
+ window.dispatchEvent(new CustomEvent('edrv:theme-change', { detail: { scheme: schemeOfSnapshot(snapshot) } }))
310
+ }
311
+ ctx.effect(() => {
312
+ if (!themeService || typeof ctx.on !== 'function') return undefined
313
+ const disposer = ctx.on('theme/change', emitTheme)
314
+ return typeof disposer === 'function' ? disposer : undefined
315
+ }, 'vscode-mode: official theme event')
316
+ ctx.effect(() => observeScheme(emitTheme), 'vscode-mode: theme attribute watch')
317
+ emitTheme()
318
+
268
319
  // 中央「文件编辑」页签(旧形态回退):类 VSCode 编辑器;侧边栏形态(官方/better-sidebar)可用时
269
320
  // 编辑器住侧边栏 Tab,本页签不注册(避免双实例:Monaco×2 + diff dock 每会话单源抢占)。
270
321
  const registerLegacyTab = (): (() => void) | null => {