dsh-vscode-mode 0.1.63 → 0.3.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/README.md +20 -0
- package/lib/client.js +1737 -98
- package/lib/client.js.map +1 -1
- package/lib/index.js +482 -20
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/commandBridge.ts +85 -0
- package/src/client/commandGlobals.ts +13 -0
- package/src/client/commandPaletteStore.ts +212 -0
- package/src/client/commandRegistry.ts +150 -0
- package/src/client/commandSearch.ts +89 -0
- package/src/client/editorModelState.ts +30 -0
- package/src/client/index.ts +32 -0
- package/src/client/keybindings.ts +71 -17
- package/src/client/monaco/loader.ts +24 -0
- package/src/client/snippets/provider.ts +172 -0
- package/src/client/styles/editor.css +51 -0
- package/src/client/ui/CommandPalette.ts +194 -0
- package/src/client/ui/ConversationDiffDock.ts +17 -9
- package/src/client/ui/EditorView.ts +185 -4
- package/src/client/ui/KeybindingsPanel.ts +1 -1
- package/src/client/ui/QuickOpen.ts +11 -3
- package/src/client/ui/SnippetsPicker.ts +296 -0
- package/src/client/ui/commandCatalog.ts +204 -0
- package/src/rpc.ts +78 -0
- package/src/shared/keybindings.ts +9 -1
- package/src/shared/rpc.ts +11 -0
- package/src/shared/snippets.ts +156 -0
- package/src/snippets.ts +427 -0
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
* dsh-vscode-mode client — 快捷键配置模块(解析/匹配/状态同步)。
|
|
3
3
|
* 纯逻辑(parseChord/parseChords/formatChord/matchEvent/chordFromEvent/normalizeKey)不依赖 DOM,可单测;
|
|
4
4
|
* 模块状态由 settings 订阅驱动(client/index.ts 调 keybindingsApply)。
|
|
5
|
-
* 键位语义:Ctrl 与 Cmd
|
|
6
|
-
*
|
|
5
|
+
* 键位语义:Ctrl 与 Cmd 互认(延续 Ctrl+P/Ctrl+B 捕获行为)。
|
|
6
|
+
* 命令目录(COMMANDS)派生自指令目录 commandCatalog(指令 = 单一数据源,设置页自动跟随);
|
|
7
|
+
* 另支持运行时键位表(第三方命令 register 时声明,不落设置 schema,注销即失效)。
|
|
8
|
+
* 作者 ddj 2026年08月26号 / 2026年09月10号
|
|
7
9
|
*/
|
|
8
10
|
import React from 'react'
|
|
9
11
|
import { KEYBINDING_DEFAULTS, normalizeKeybindings } from '../shared/keybindings.js'
|
|
12
|
+
import { BRIDGE_COMMANDS, EDITOR_COMMANDS, showCommandsDef } from './ui/commandCatalog.js'
|
|
13
|
+
import { log } from './log.js'
|
|
10
14
|
|
|
11
15
|
/** 解析后的键位(修饰符 + 规范化主键)。 */
|
|
12
16
|
export interface Binding {
|
|
@@ -19,24 +23,39 @@ export interface Binding {
|
|
|
19
23
|
|
|
20
24
|
/** 命令目录(设置页展示标签;执行按目录序先匹配先执行,冲突键位确定性)。 */
|
|
21
25
|
export const COMMANDS: Array<{ id: string; label: string }> = [
|
|
22
|
-
{ id:
|
|
23
|
-
{ id: 'edrv.
|
|
24
|
-
{ id:
|
|
25
|
-
{ id: 'edrv.searchInFiles', label: '在工作区中搜索' },
|
|
26
|
-
{ id: 'edrv.navigateBack', label: '后退(导航历史)' },
|
|
27
|
-
{ id: 'edrv.navigateForward', label: '前进(导航历史)' },
|
|
28
|
-
{ id: 'edrv.nextTab', label: '下一个页签' },
|
|
29
|
-
{ id: 'edrv.prevTab', label: '上一个页签' },
|
|
26
|
+
...EDITOR_COMMANDS.map((command) => ({ id: command.id, label: command.label })),
|
|
27
|
+
{ id: 'edrv.showCommands', label: '显示所有命令' },
|
|
28
|
+
...BRIDGE_COMMANDS.map((command) => ({ id: command.id, label: command.label })),
|
|
30
29
|
]
|
|
31
30
|
|
|
31
|
+
/** 命令栏命令定义(键位自检与目录展示共用;run 不参与键位逻辑)。 */
|
|
32
|
+
function paletteCommand(): { id: string; label: string; keybinding?: string } {
|
|
33
|
+
return showCommandsDef(() => {})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 目录与默认键位表一致性自检(仅告警不中断):新指令漏写共享表时第一时间可见。
|
|
38
|
+
* @author ddj 2026年09月10号
|
|
39
|
+
*/
|
|
40
|
+
function checkDefaultsDrift(): void {
|
|
41
|
+
for (const command of [...EDITOR_COMMANDS, paletteCommand()]) {
|
|
42
|
+
if (!command.keybinding) continue
|
|
43
|
+
if (KEYBINDING_DEFAULTS[command.id] === command.keybinding) continue
|
|
44
|
+
log.warn('指令默认键位与共享表不一致:' + command.id
|
|
45
|
+
+ ' 目录=' + command.keybinding + ' 共享表=' + String(KEYBINDING_DEFAULTS[command.id]))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
const MODIFIERS: Record<string, 'ctrl' | 'shift' | 'alt' | 'meta'> = {
|
|
33
50
|
ctrl: 'ctrl', cmd: 'meta', meta: 'meta', shift: 'shift', alt: 'alt',
|
|
34
51
|
}
|
|
35
52
|
|
|
36
53
|
let current: Record<string, string> = { ...KEYBINDING_DEFAULTS }
|
|
37
|
-
|
|
54
|
+
const runtime = new Map<string, Binding[]>()
|
|
38
55
|
const listeners = new Set<() => void>()
|
|
39
56
|
|
|
57
|
+
checkDefaultsDrift()
|
|
58
|
+
|
|
40
59
|
/**
|
|
41
60
|
* 应用设置快照(与默认值合并;未知 id 丢弃;空对象 = 全部默认)。
|
|
42
61
|
* 每个命令可含多候选键位(`|` 分隔),任一命中即触发。
|
|
@@ -45,8 +64,6 @@ const listeners = new Set<() => void>()
|
|
|
45
64
|
*/
|
|
46
65
|
export function keybindingsApply(raw: unknown): void {
|
|
47
66
|
current = { ...KEYBINDING_DEFAULTS, ...normalizeKeybindings(raw) }
|
|
48
|
-
parsed = {}
|
|
49
|
-
for (const id of Object.keys(current)) parsed[id] = parseChords(current[id])
|
|
50
67
|
for (const listener of listeners) {
|
|
51
68
|
try { listener() } catch { /* 监听器异常不影响其他订阅 */ }
|
|
52
69
|
}
|
|
@@ -64,24 +81,61 @@ export function subscribeKeybindings(listener: () => void): () => void {
|
|
|
64
81
|
}
|
|
65
82
|
|
|
66
83
|
/**
|
|
67
|
-
*
|
|
68
|
-
* @author ddj 2026年
|
|
84
|
+
* 注册运行时键位(第三方命令;优先于设置值,注销即失效)。
|
|
85
|
+
* @author ddj 2026年09月10号
|
|
86
|
+
* @param id 命令 id
|
|
87
|
+
* @param chord 键位弦(空/非法按未绑定处理)
|
|
88
|
+
* @returns 注销函数(幂等)
|
|
89
|
+
*/
|
|
90
|
+
export function addRuntimeKeybinding(id: string, chord: string): () => void {
|
|
91
|
+
runtime.set(id, parseChords(chord))
|
|
92
|
+
notifyKeybindings()
|
|
93
|
+
return () => removeRuntimeKeybinding(id)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 移除运行时键位。
|
|
98
|
+
* @author ddj 2026年09月10号
|
|
99
|
+
* @param id 命令 id
|
|
100
|
+
*/
|
|
101
|
+
export function removeRuntimeKeybinding(id: string): void {
|
|
102
|
+
if (!runtime.delete(id)) return
|
|
103
|
+
notifyKeybindings()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 通知键位订阅者(异常隔离)。 */
|
|
107
|
+
function notifyKeybindings(): void {
|
|
108
|
+
for (const listener of listeners) {
|
|
109
|
+
try { listener() } catch { /* 监听器异常不影响其他订阅 */ }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 当前命令的键位弦(运行时键位优先;未绑定/空 → null)。
|
|
115
|
+
* @author ddj 2026年08月26号 / 2026年09月10号
|
|
69
116
|
* @param id 命令 id
|
|
70
117
|
* @returns 键位弦或 null
|
|
71
118
|
*/
|
|
72
119
|
export function chordOf(id: string): string | null {
|
|
120
|
+
if (runtime.has(id)) {
|
|
121
|
+
const chords = runtime.get(id) ?? []
|
|
122
|
+
return chords.length ? chords.map(formatChord).join('|') : null
|
|
123
|
+
}
|
|
73
124
|
const chord = current[id]
|
|
74
125
|
return typeof chord === 'string' && chord.trim() !== '' ? chord : null
|
|
75
126
|
}
|
|
76
127
|
|
|
77
128
|
/**
|
|
78
129
|
* 当前命令的解析键位集合(未绑定/非法 → 空数组;含多候选)。
|
|
79
|
-
* @author ddj 2026年08月26号
|
|
130
|
+
* @author ddj 2026年08月26号 / 2026年09月10号
|
|
80
131
|
* @param id 命令 id
|
|
81
132
|
* @returns 解析键位数组(可能为空)
|
|
82
133
|
*/
|
|
83
134
|
export function bindingsOf(id: string): Binding[] {
|
|
84
|
-
|
|
135
|
+
const override = runtime.get(id)
|
|
136
|
+
if (override) return override
|
|
137
|
+
const chord = current[id]
|
|
138
|
+
return typeof chord === 'string' ? parseChords(chord) : []
|
|
85
139
|
}
|
|
86
140
|
|
|
87
141
|
/**
|
|
@@ -65,6 +65,8 @@ export const LANG_BY_EXT = {
|
|
|
65
65
|
lua51: 'lua', luac: 'lua',
|
|
66
66
|
gitattributes: 'ini', editorconfig: 'ini', env: 'ini', properties: 'ini',
|
|
67
67
|
json5: 'jsonc', log: 'plaintext', txt: 'plaintext',
|
|
68
|
+
// 代码片段文件按 JSON 高亮(VS Code 同款:.code-snippets 是带注释的 JSON)
|
|
69
|
+
'code-snippets': 'json',
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
/**
|
|
@@ -81,6 +83,28 @@ export function langOf(path) {
|
|
|
81
83
|
return LANG_BY_EXT[base.slice(dot + 1).toLowerCase()] ?? 'plaintext'
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
/** 片段文件后缀(判断「文件自身是片段文件」而非普通源码)。 */
|
|
87
|
+
const SNIPPET_FILE_SUFFIX = '.code-snippets'
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 片段文件 → 它绑定的语言 id(VS Code 文件名约定 `<language>.code-snippets`)。
|
|
91
|
+
*
|
|
92
|
+
* 为什么不能直接用 {@link langOf}:`.code-snippets` 本身在 LANG_BY_EXT 里映射为 json
|
|
93
|
+
* (编辑片段文件时要按 JSON 高亮),于是 langOf('lua.code-snippets') 会得到 'json' 而不是
|
|
94
|
+
* 'lua' —— 曾导致「新建」默认文件名被算成 `json.code-snippets`。故这里按片段命名约定
|
|
95
|
+
* 单独解析:去掉 `.code-snippets` 后缀取语言前缀。
|
|
96
|
+
*
|
|
97
|
+
* @author ddj 2026年09月10号
|
|
98
|
+
* @param path 片段文件路径
|
|
99
|
+
* @returns 语言 id(`global.code-snippets` / 无法识别 → 空串 = 全语言)
|
|
100
|
+
*/
|
|
101
|
+
export function snippetLanguageOf(path) {
|
|
102
|
+
const base = String(path || '').split(/[\\/]/).pop() || ''
|
|
103
|
+
if (!base.toLowerCase().endsWith(SNIPPET_FILE_SUFFIX)) return langOf(path)
|
|
104
|
+
const lang = base.slice(0, -SNIPPET_FILE_SUFFIX.length).toLowerCase()
|
|
105
|
+
return lang === 'global' ? '' : lang
|
|
106
|
+
}
|
|
107
|
+
|
|
84
108
|
/**
|
|
85
109
|
* 加载 Monaco Editor(AMD 构建,随插件包离线分发):注入 loader.js → require.config → editor.main。
|
|
86
110
|
* @author ddj 2026年08月20号
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — 代码片段补全 provider(Monaco IntelliSense)。
|
|
4
|
+
* 数据源:host snippets.entries(全局 ~/.dsh/snippets + 当前工作区 <工作区>/.dsh/snippets),
|
|
5
|
+
* 条目按 model 语言过滤(language 空串 = 全语言);条目主体交给 Monaco 片段语法解析
|
|
6
|
+
* (${1:占位} / $TM_FILENAME 等由 InsertAsSnippet 规则展开)。
|
|
7
|
+
* 性能:条目模块级缓存 + TTL,不在每次按键打 RPC(与 ai/inlineProvider 的去抖策略异曲同工,
|
|
8
|
+
* 但片段是本地小表,缓存足矣);edrv:snippets-changed(配置文件保存后)强制失效。
|
|
9
|
+
* 作者 ddj 2026年09月10号
|
|
10
|
+
*/
|
|
11
|
+
import { rpc } from '../rpc.js'
|
|
12
|
+
|
|
13
|
+
/** 条目缓存 TTL(毫秒):片段文件本就只在用户编辑时变化。 */
|
|
14
|
+
const CACHE_TTL_MS = 10_000
|
|
15
|
+
|
|
16
|
+
/** 补全候选上限(超大片段库下防拖慢渲染)。 */
|
|
17
|
+
const MAX_ITEMS = 200
|
|
18
|
+
|
|
19
|
+
/** 已加载条目缓存(null = 未加载/已失效)。 */
|
|
20
|
+
let cache = null
|
|
21
|
+
let cacheAt = 0
|
|
22
|
+
let pending = null
|
|
23
|
+
let registered = false
|
|
24
|
+
let disposer = null
|
|
25
|
+
|
|
26
|
+
/** 当前会话 id(补全按会话工作区叠加项目片段;EditorView 装配时注入)。 */
|
|
27
|
+
let sessionId = null
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 设置当前会话(会话切换时更新;缓存一并失效,使项目片段跟随工作区)。
|
|
31
|
+
* @author ddj 2026年09月10号
|
|
32
|
+
* @param id 会话 id
|
|
33
|
+
*/
|
|
34
|
+
export function setSnippetsSession(id) {
|
|
35
|
+
if (sessionId === id) return
|
|
36
|
+
sessionId = id
|
|
37
|
+
invalidateSnippets()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 失效条目缓存(配置文件保存/会话切换后调用)。
|
|
42
|
+
* @author ddj 2026年09月10号
|
|
43
|
+
*/
|
|
44
|
+
export function invalidateSnippets() {
|
|
45
|
+
cache = null
|
|
46
|
+
cacheAt = 0
|
|
47
|
+
pending = null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 读取条目(带缓存与在飞去重);失败返回空表(补全静默降级,不打断输入)。
|
|
52
|
+
* @author ddj 2026年09月10号
|
|
53
|
+
* @returns 片段条目数组
|
|
54
|
+
*/
|
|
55
|
+
async function loadEntries() {
|
|
56
|
+
if (cache && Date.now() - cacheAt < CACHE_TTL_MS) return cache
|
|
57
|
+
if (pending) return pending
|
|
58
|
+
pending = rpc('snippets.entries', { sessionId })
|
|
59
|
+
.then((res) => {
|
|
60
|
+
const entries = res && res.ok && Array.isArray(res.entries) ? res.entries : []
|
|
61
|
+
cache = entries
|
|
62
|
+
cacheAt = Date.now()
|
|
63
|
+
return entries
|
|
64
|
+
})
|
|
65
|
+
.catch(() => {
|
|
66
|
+
cache = []
|
|
67
|
+
cacheAt = Date.now()
|
|
68
|
+
return cache
|
|
69
|
+
})
|
|
70
|
+
.finally(() => { pending = null })
|
|
71
|
+
return pending
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 按语言过滤条目:条目语言为空(全语言)或与 model 语言一致即命中。
|
|
76
|
+
* 项目片段(scope=project)排在全局之后去重,同名同前缀时项目优先。
|
|
77
|
+
* @author ddj 2026年09月10号
|
|
78
|
+
* @param entries 全部条目
|
|
79
|
+
* @param languageId 当前模型语言
|
|
80
|
+
* @returns 生效条目(项目优先,已去重)
|
|
81
|
+
*/
|
|
82
|
+
export function entriesForLanguage(entries, languageId) {
|
|
83
|
+
const lang = String(languageId ?? '').toLowerCase()
|
|
84
|
+
const global = []
|
|
85
|
+
const project = []
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
const entryLang = String(entry?.language ?? '').toLowerCase()
|
|
88
|
+
if (entryLang && entryLang !== lang) continue
|
|
89
|
+
if (entry?.scope === 'project') project.push(entry)
|
|
90
|
+
else global.push(entry)
|
|
91
|
+
}
|
|
92
|
+
// 项目条目覆盖同 key 的全局条目(工作区定制优先)
|
|
93
|
+
const projectKeys = new Set(project.map((entry) => entry.key))
|
|
94
|
+
const merged = global.filter((entry) => !projectKeys.has(entry.key)).concat(project)
|
|
95
|
+
return merged.slice(0, MAX_ITEMS)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 注册代码片段补全 provider(幂等;Monaco 就绪后调用一次)。
|
|
100
|
+
* @author ddj 2026年09月10号
|
|
101
|
+
* @param monaco window.monaco
|
|
102
|
+
*/
|
|
103
|
+
export function registerSnippetProvider(monaco) {
|
|
104
|
+
if (registered || !monaco?.languages?.registerCompletionItemProvider) return
|
|
105
|
+
const snippetKind = monaco.languages.CompletionItemKind?.Snippet
|
|
106
|
+
const asSnippet = monaco.languages.CompletionItemInsertTextRule?.InsertAsSnippet
|
|
107
|
+
// 缺少片段枚举(精简版 Monaco)时不注册:否则候选项会退化为纯文本插入,误导用户
|
|
108
|
+
if (typeof snippetKind !== 'number' || typeof asSnippet !== 'number') return
|
|
109
|
+
registered = true
|
|
110
|
+
disposer = monaco.languages.registerCompletionItemProvider('*', {
|
|
111
|
+
async provideCompletionItems(model, position) {
|
|
112
|
+
const entries = await loadEntries()
|
|
113
|
+
if (!entries.length) return { suggestions: [] }
|
|
114
|
+
const items = entriesForLanguage(entries, model?.getLanguageId?.())
|
|
115
|
+
if (!items.length) return { suggestions: [] }
|
|
116
|
+
const word = model.getWordUntilPosition(position)
|
|
117
|
+
const range = {
|
|
118
|
+
startLineNumber: position.lineNumber,
|
|
119
|
+
endLineNumber: position.lineNumber,
|
|
120
|
+
startColumn: word.startColumn,
|
|
121
|
+
endColumn: word.endColumn,
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
suggestions: items.map((entry) => ({
|
|
125
|
+
label: entry.prefix || entry.key,
|
|
126
|
+
// 无前缀条目不经键入触发(靠「插入代码片段」命令使用),补全里不展示空标签
|
|
127
|
+
insertText: entry.body,
|
|
128
|
+
detail: (entry.scope === 'project' ? '项目片段 · ' : '全局片段 · ') + entry.file,
|
|
129
|
+
documentation: entry.description || entry.key,
|
|
130
|
+
kind: snippetKind,
|
|
131
|
+
insertTextRules: asSnippet,
|
|
132
|
+
filterText: entry.prefix || entry.key,
|
|
133
|
+
range,
|
|
134
|
+
})).filter((item) => Boolean(item.label)),
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 装配:Monaco 就绪后注册 provider(幂等)。
|
|
142
|
+
* @author ddj 2026年09月10号
|
|
143
|
+
* @param monaco window.monaco
|
|
144
|
+
*/
|
|
145
|
+
export function setupSnippets(monaco) {
|
|
146
|
+
registerSnippetProvider(monaco)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 卸载:注销 provider(插件热重载/卸载时调用)。
|
|
151
|
+
* @author ddj 2026年09月10号
|
|
152
|
+
*/
|
|
153
|
+
export function disposeSnippets() {
|
|
154
|
+
if (typeof disposer === 'function') {
|
|
155
|
+
try { disposer.dispose?.() } catch { /* 已注销 */ }
|
|
156
|
+
}
|
|
157
|
+
disposer = null
|
|
158
|
+
registered = false
|
|
159
|
+
invalidateSnippets()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 读取可插入条目(「插入代码片段」命令用;含无前缀条目)。
|
|
164
|
+
* @author ddj 2026年09月10号
|
|
165
|
+
* @param languageId 当前模型语言(缺省返回全语言 + 该语言条目)
|
|
166
|
+
* @returns 生效条目数组
|
|
167
|
+
*/
|
|
168
|
+
export async function listSnippetsFor(languageId) {
|
|
169
|
+
const entries = await loadEntries()
|
|
170
|
+
if (!languageId) return entries.slice(0, MAX_ITEMS)
|
|
171
|
+
return entriesForLanguage(entries, languageId)
|
|
172
|
+
}
|
|
@@ -369,5 +369,56 @@
|
|
|
369
369
|
[data-edrv-view] .edrv-rules-save { height: 24px; padding: 0 14px; border: none; border-radius: 6px; background: var(--dsw-alias-brand-primary, #0f9d58); color: var(--dsw-alias-label-primary-inverted, #fff); font-size: 12px; cursor: pointer; }
|
|
370
370
|
[data-edrv-view] .edrv-rules-save:disabled { opacity: .5; cursor: default; }
|
|
371
371
|
|
|
372
|
+
/* 命令栏(Ctrl+Shift+P / F1):body 级 portal 浮层,故不依赖编辑区几何。
|
|
373
|
+
⚠️ 浮层 z-index 阶梯(自下而上;改动务必保持顺序,tests/overlayZOrder.test.ts 守约):
|
|
374
|
+
编辑器内浮层 60(.edrv-search-pop) / 71(.edrv-ctxmenu)
|
|
375
|
+
< 190 命令栏遮罩 < 200 命令栏
|
|
376
|
+
< 195 片段浮窗遮罩 < 197 片段二级弹窗遮罩
|
|
377
|
+
命令栏必须**高于**片段浮窗:曾因片段遮罩(192) 压过命令栏(191),导致片段浮窗打开时
|
|
378
|
+
命令栏被整体盖住,按 Ctrl+Shift+P 因「已打开即早退」而毫无反应(表现为快捷键失效)。 */
|
|
379
|
+
[data-edrv-view] .edrv-palette-mask { position: fixed; inset: 0; z-index: 190; background: rgba(0, 0, 0, .28); }
|
|
380
|
+
[data-edrv-view] .edrv-palette { position: fixed; top: 12vh; left: 50%; transform: translateX(-50%); z-index: 200; width: min(580px, calc(100vw - 32px)); max-height: 64vh; display: flex; flex-direction: column; border-radius: 10px; background: var(--dsw-alias-bg-overlay, #ffffff); border: 1px solid var(--dsw-alias-border-l2, #cbd2d9); box-shadow: 0 12px 34px rgba(31, 41, 51, .24); overflow: hidden; }
|
|
381
|
+
[data-edrv-view] .edrv-palette-input { height: 38px; flex: 0 0 auto; padding: 0 12px; border: none; border-bottom: 1px solid var(--dsw-alias-border-l1, #e0e6e8); background: transparent; color: var(--dsw-alias-label-primary, #1f2933); font-size: 13px; outline: none; }
|
|
382
|
+
[data-edrv-view] .edrv-palette-list { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 4px; }
|
|
383
|
+
[data-edrv-view] .edrv-palette-row { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: 6px; font-size: 12px; color: var(--dsw-alias-label-primary, #1f2933); cursor: pointer; }
|
|
384
|
+
[data-edrv-view] .edrv-palette-row.edrv-palette-sel { background: var(--dsw-alias-interactive-bg-hover, rgba(15, 157, 88, .10)); }
|
|
385
|
+
[data-edrv-view] .edrv-palette-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
386
|
+
[data-edrv-view] .edrv-palette-cat { flex: 0 0 auto; font-size: 10px; padding: 1px 7px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-tertiary, #7b8794); }
|
|
387
|
+
[data-edrv-view] .edrv-palette-key { flex: 0 0 auto; font-family: var(--ds-font-family-code, ui-monospace, monospace); font-size: 10px; padding: 1px 6px; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 5px; background: var(--dsw-alias-bg-layer-1, #f7f9fa); color: var(--dsw-alias-label-secondary, #52606d); }
|
|
388
|
+
[data-edrv-view] .edrv-palette-empty { padding: 14px 10px; font-size: 12px; color: var(--dsw-alias-label-tertiary, #7b8794); text-align: center; }
|
|
389
|
+
[data-edrv-view] .edrv-palette-notice { flex: 0 0 auto; padding: 6px 10px; border-top: 1px solid var(--dsw-alias-border-l1, #e0e6e8); font-size: 11px; color: var(--dsw-alias-state-warn-primary, #b7791f); }
|
|
390
|
+
|
|
391
|
+
/* 代码片段选择器(配置 / 插入):居中浮窗 + 新建二级弹窗。
|
|
392
|
+
卡片复用 mcp.css 的 .vsm-mcp-dialog(与「添加 MCP」「工作区选择」同一视觉语言),
|
|
393
|
+
此处只补遮罩、尺寸与列表布局。⚠️ 根节点须带 data-edrv-view,否则这些规则全部失配。 */
|
|
394
|
+
[data-edrv-view] .edrv-snip-mask { position: fixed; inset: 0; z-index: 195; display: flex; align-items: center; justify-content: center; padding: 24px; box-sizing: border-box; background: var(--dsw-alias-bg-mask-drop, rgba(0, 0, 0, .55)); }
|
|
395
|
+
/* 二级弹窗叠在一级浮窗之上(仍在命令栏之下,见上方阶梯说明) */
|
|
396
|
+
[data-edrv-view] .edrv-snip-mask-top { z-index: 197; }
|
|
397
|
+
[data-edrv-view] .edrv-snip-dialog { display: flex; flex-direction: column; overflow: hidden; width: min(620px, calc(100vw - 48px)); }
|
|
398
|
+
[data-edrv-view] .edrv-snip-hint { margin: 0 0 12px; font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-tertiary, #9aa5b1); }
|
|
399
|
+
[data-edrv-view] .edrv-snip-list { display: flex; flex-direction: column; gap: 1px; min-height: 0; max-height: 46vh; overflow: auto; }
|
|
400
|
+
[data-edrv-view] .edrv-snip-group { padding: 12px 2px 4px; font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-secondary, #aaa); }
|
|
401
|
+
[data-edrv-view] .edrv-snip-row { display: flex; align-items: center; gap: 4px; width: 100%; box-sizing: border-box; padding: 2px; border: none; border-radius: 8px; background: transparent; text-align: left; color: var(--dsw-alias-label-primary, #e6e6e6); font: inherit; }
|
|
402
|
+
[data-edrv-view] .edrv-snip-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, .06)); }
|
|
403
|
+
[data-edrv-view] button.edrv-snip-row { cursor: pointer; }
|
|
404
|
+
[data-edrv-view] .edrv-snip-row-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; padding: 8px 10px; border: none; border-radius: 6px; background: transparent; text-align: left; cursor: pointer; color: inherit; font: inherit; }
|
|
405
|
+
[data-edrv-view] .edrv-snip-label { font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary, #e6e6e6); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
406
|
+
[data-edrv-view] .edrv-snip-desc { font-size: 11px; color: var(--dsw-alias-label-tertiary, #9aa5b1); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
407
|
+
[data-edrv-view] .edrv-snip-src { flex: 0 0 auto; padding-right: 6px; font-size: 11px; color: var(--dsw-alias-label-tertiary, #9aa5b1); }
|
|
408
|
+
[data-edrv-view] .edrv-snip-warn { flex: 0 0 auto; font-size: 11px; padding: 2px 7px; border-radius: 999px; background: var(--dsw-alias-state-warn-tertiary, rgba(229, 185, 63, .16)); color: var(--dsw-alias-state-warn-primary, #e5b93f); white-space: nowrap; }
|
|
409
|
+
[data-edrv-view] .edrv-snip-act { flex: 0 0 auto; width: 26px; height: 26px; border: none; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 13px; line-height: 1; cursor: pointer; }
|
|
410
|
+
[data-edrv-view] .edrv-snip-act:hover { background: var(--dsw-alias-interactive-bg-hover-danger, rgba(217, 83, 79, .16)); color: var(--dsw-alias-state-error-primary, #f07878); }
|
|
411
|
+
[data-edrv-view] .edrv-snip-empty { padding: 12px 4px; font-size: 12px; color: var(--dsw-alias-label-tertiary, #9aa5b1); line-height: 1.6; }
|
|
412
|
+
/* 当前文件语言提示条(片段按语言绑定,明确告知生效范围) */
|
|
413
|
+
[data-edrv-view] .edrv-snip-langbar { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; margin: 0 0 10px; padding: 8px 10px; border-radius: 6px; background: var(--dsw-alias-bg-layer-1, #2b2b2b); font-size: 12px; color: var(--dsw-alias-label-secondary, #aaa); }
|
|
414
|
+
[data-edrv-view] .edrv-snip-langbar b { color: var(--dsw-alias-label-primary, #e6e6e6); }
|
|
415
|
+
/* 二级弹窗的动作按钮(原生 .vsm-mcp-dialog-actions 已提供排版与按钮样式) */
|
|
416
|
+
[data-edrv-view] .edrv-snip-dialog .vsm-mcp-dialog-actions { flex-wrap: wrap; }
|
|
417
|
+
/* 删除图标按钮:.vsm-danger 的规则全部限定在 .vsm-mcp-actions button 之下,裸用无样式,此处补自有类 */
|
|
418
|
+
[data-edrv-view] .edrv-snip-act.edrv-snip-danger:hover { background: var(--dsw-alias-interactive-bg-hover-danger, rgba(217, 83, 79, .16)); color: var(--dsw-alias-state-error-primary, #f07878); }
|
|
419
|
+
/* 浮窗内的原生空态(.vsm-mcp-empty 默认为整页尺寸)压缩为对话框尺寸 */
|
|
420
|
+
[data-edrv-view] .edrv-snip-dialog .vsm-mcp-empty { padding: 26px 16px; border: 0; }
|
|
421
|
+
|
|
422
|
+
|
|
372
423
|
|
|
373
424
|
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 命令栏浮层(Ctrl+Shift+P / F1)。
|
|
3
|
+
* 订阅 commandPaletteStore 的开关状态;候选来自注入的指令注册表(window.dsh.edrvCommands),
|
|
4
|
+
* 过滤/排序复用 commandSearch 纯函数;↑↓ 选择、Enter 执行、Esc 或点击遮罩关闭。
|
|
5
|
+
* 浮层经 createPortal 渲染到 body,故不受编辑区三种形态(官方侧栏/better-sidebar/中央页签)影响;
|
|
6
|
+
* 宿主单实例由 store 的 claimPaletteHost 保证(另一个宿主自动返回 null)。
|
|
7
|
+
* 作者 ddj 2026年09月10号
|
|
8
|
+
*/
|
|
9
|
+
import React from 'react'
|
|
10
|
+
import { createPortal } from 'react-dom'
|
|
11
|
+
import { REGISTRY_GLOBAL } from '../commandGlobals.js'
|
|
12
|
+
import { filterCommands } from '../commandSearch.js'
|
|
13
|
+
import { createCommandRegistry } from '../commandRegistry.js'
|
|
14
|
+
import type { CommandRegistry } from '../commandRegistry.js'
|
|
15
|
+
import { chordOf } from '../keybindings.js'
|
|
16
|
+
import {
|
|
17
|
+
claimPaletteHost, closeCommandPalette, isPaletteOpen, paletteHostRev, paletteOpenSeq, registryRef,
|
|
18
|
+
releasePaletteHost, runPaletteCommand, subscribePalette,
|
|
19
|
+
} from '../commandPaletteStore.js'
|
|
20
|
+
import type { CommandDef } from './commandCatalog.js'
|
|
21
|
+
|
|
22
|
+
/** 空注册表(宿主未注入时的安全降级:命令栏可开但无候选)。 */
|
|
23
|
+
let fallbackRegistry: CommandRegistry | null = null
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 读取指令注册表:① 装配期存入的模块引用(首选,不依赖全局)
|
|
27
|
+
* ② window[REGISTRY_GLOBAL] 镜像(第三方/调试场景)③ 空表降级(仅装配异常时)。
|
|
28
|
+
* @author ddj 2026年09月10号
|
|
29
|
+
* @returns 指令注册表
|
|
30
|
+
*/
|
|
31
|
+
function useRegistry(): CommandRegistry {
|
|
32
|
+
const injected = registryRef()
|
|
33
|
+
if (injected) return injected
|
|
34
|
+
const mirrored = typeof window === 'undefined'
|
|
35
|
+
? undefined
|
|
36
|
+
: (window as unknown as Record<string, CommandRegistry | undefined>)[REGISTRY_GLOBAL]
|
|
37
|
+
if (mirrored) return mirrored
|
|
38
|
+
if (!fallbackRegistry) fallbackRegistry = createCommandRegistry()
|
|
39
|
+
return fallbackRegistry
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 命令栏每一行的展示信息。 */
|
|
43
|
+
interface PaletteRow {
|
|
44
|
+
command: CommandDef
|
|
45
|
+
chord: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 组装候选行(先按可用性过滤,再按查询排序)。
|
|
50
|
+
* @author ddj 2026年09月10号
|
|
51
|
+
* @param registry 指令注册表
|
|
52
|
+
* @param query 用户输入
|
|
53
|
+
* @returns 候选行
|
|
54
|
+
*/
|
|
55
|
+
function rowsOf(registry: CommandRegistry, query: string): PaletteRow[] {
|
|
56
|
+
return filterCommands(registry.available(), query)
|
|
57
|
+
.map((command) => ({ command, chord: chordOf(command.id) }))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 命令栏浮层(未展开/非宿主时渲染 null)。
|
|
62
|
+
* @author ddj 2026年09月10号
|
|
63
|
+
* @returns 浮层 React 元素或 null
|
|
64
|
+
*/
|
|
65
|
+
export function CommandPalette(): React.ReactElement | null {
|
|
66
|
+
const open = React.useSyncExternalStore(subscribePalette, isPaletteOpen)
|
|
67
|
+
// 唤起序号:已打开时每次快捷键再触发都会递增 → 重新聚焦输入框(否则看似"没反应")
|
|
68
|
+
const openSeq = React.useSyncExternalStore(subscribePalette, paletteOpenSeq)
|
|
69
|
+
// 宿主令牌版本:宿主释放时递增,驱动本实例重试认领(避免宿主更替后无人渲染浮层)
|
|
70
|
+
const hostRev = React.useSyncExternalStore(subscribePalette, paletteHostRev)
|
|
71
|
+
const registry = useRegistry()
|
|
72
|
+
const [token, setToken] = React.useState<object | null>(null)
|
|
73
|
+
const [query, setQuery] = React.useState('')
|
|
74
|
+
const [selected, setSelected] = React.useState(0)
|
|
75
|
+
const [notice, setNotice] = React.useState('')
|
|
76
|
+
const inputRef = React.useRef<HTMLInputElement | null>(null)
|
|
77
|
+
const tokenRef = React.useRef<object | null>(null)
|
|
78
|
+
const host = token !== null
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 认领宿主:放在 effect(不在 render 期做副作用 —— StrictMode 双调用会让首次认领
|
|
82
|
+
* 被判定失败而永久无人渲染)。hostRev 变化(有实例释放了宿主)时重试认领。
|
|
83
|
+
*
|
|
84
|
+
* ⚠️ 此处**不**做 cleanup 释放:releasePaletteHost 会通知订阅者(hostRev 变化),
|
|
85
|
+
* 若在依赖 hostRev 的 effect 里释放,会「释放→通知→重跑→再释放」自激循环。
|
|
86
|
+
* 释放统一交给下面的「仅卸载时」effect。
|
|
87
|
+
*/
|
|
88
|
+
React.useEffect(() => {
|
|
89
|
+
if (tokenRef.current) return
|
|
90
|
+
const claimed = claimPaletteHost()
|
|
91
|
+
if (!claimed) return
|
|
92
|
+
tokenRef.current = claimed
|
|
93
|
+
setToken(claimed)
|
|
94
|
+
}, [hostRev])
|
|
95
|
+
|
|
96
|
+
/** 仅卸载时释放宿主;释放后 store 通知其余实例接管(自愈)。 */
|
|
97
|
+
React.useEffect(() => () => {
|
|
98
|
+
releasePaletteHost(tokenRef.current)
|
|
99
|
+
tokenRef.current = null
|
|
100
|
+
}, [])
|
|
101
|
+
// 新开(open false→true)时重置查询/选择/提示
|
|
102
|
+
React.useEffect(() => {
|
|
103
|
+
if (!open) return
|
|
104
|
+
setQuery('')
|
|
105
|
+
setSelected(0)
|
|
106
|
+
setNotice('')
|
|
107
|
+
}, [open])
|
|
108
|
+
// 每次唤起(含已打开时的重复唤起 openSeq 变化)重新聚焦输入框 ——
|
|
109
|
+
// 否则命令栏被其它浮层短暂遮挡后,快捷键再按会「看似没反应」。
|
|
110
|
+
React.useEffect(() => {
|
|
111
|
+
if (!open) return undefined
|
|
112
|
+
const timer = setTimeout(() => {
|
|
113
|
+
const input = inputRef.current
|
|
114
|
+
if (!input) return
|
|
115
|
+
input.focus()
|
|
116
|
+
input.select?.()
|
|
117
|
+
}, 0)
|
|
118
|
+
return () => clearTimeout(timer)
|
|
119
|
+
}, [open, openSeq])
|
|
120
|
+
|
|
121
|
+
const rows = React.useMemo(
|
|
122
|
+
() => (open && host ? rowsOf(registry, query) : []),
|
|
123
|
+
[open, host, registry, query],
|
|
124
|
+
)
|
|
125
|
+
const active = rows.length ? Math.min(selected, rows.length - 1) : 0
|
|
126
|
+
|
|
127
|
+
React.useEffect(() => {
|
|
128
|
+
if (!open) return
|
|
129
|
+
const node = document.querySelector('.edrv-palette-row.edrv-palette-sel')
|
|
130
|
+
const scroll = (node as HTMLElement | null)?.scrollIntoView
|
|
131
|
+
if (typeof scroll === 'function') scroll.call(node, { block: 'nearest' })
|
|
132
|
+
}, [open, active, rows.length])
|
|
133
|
+
|
|
134
|
+
/** 执行候选行(执行器内部会做可用性校验与异常上报)。 */
|
|
135
|
+
const execute = (row: PaletteRow | undefined): void => {
|
|
136
|
+
if (!row) return
|
|
137
|
+
closeCommandPalette()
|
|
138
|
+
if (!runPaletteCommand(row.command.id)) setNotice('命令未执行:' + row.command.label)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
|
142
|
+
event.stopPropagation()
|
|
143
|
+
if (event.key === 'Escape') {
|
|
144
|
+
event.preventDefault()
|
|
145
|
+
closeCommandPalette()
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
149
|
+
event.preventDefault()
|
|
150
|
+
if (!rows.length) return
|
|
151
|
+
const step = event.key === 'ArrowDown' ? 1 : rows.length - 1
|
|
152
|
+
setSelected((value) => (Math.min(value, rows.length - 1) + step) % rows.length)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
if (event.key === 'Enter') {
|
|
156
|
+
event.preventDefault()
|
|
157
|
+
execute(rows[active])
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!open || !host || typeof document === 'undefined') return null
|
|
162
|
+
const list = rows.length
|
|
163
|
+
? rows.map((row, index) => React.createElement('div', {
|
|
164
|
+
key: row.command.id,
|
|
165
|
+
className: 'edrv-palette-row' + (index === active ? ' edrv-palette-sel' : ''),
|
|
166
|
+
onMouseEnter: () => setSelected(index),
|
|
167
|
+
onClick: () => execute(row),
|
|
168
|
+
},
|
|
169
|
+
React.createElement('span', { className: 'edrv-palette-label' }, row.command.label),
|
|
170
|
+
React.createElement('span', { className: 'edrv-palette-cat' }, row.command.category),
|
|
171
|
+
row.chord ? React.createElement('kbd', { className: 'edrv-palette-key' }, row.chord) : null))
|
|
172
|
+
: React.createElement('div', { className: 'edrv-palette-empty' }, '无匹配命令')
|
|
173
|
+
|
|
174
|
+
const panel = React.createElement('div', { className: 'edrv-palette', 'data-edrv-palette': '1', onKeyDown },
|
|
175
|
+
React.createElement('input', {
|
|
176
|
+
ref: inputRef,
|
|
177
|
+
className: 'edrv-palette-input',
|
|
178
|
+
value: query,
|
|
179
|
+
placeholder: '输入命令名称…(↑↓ 选择 · Enter 执行 · Esc 关闭)',
|
|
180
|
+
spellCheck: false,
|
|
181
|
+
onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
182
|
+
setQuery(event.target.value)
|
|
183
|
+
setSelected(0)
|
|
184
|
+
setNotice('')
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
React.createElement('div', { className: 'edrv-palette-list' }, list),
|
|
188
|
+
notice ? React.createElement('div', { className: 'edrv-palette-notice' }, notice) : null)
|
|
189
|
+
|
|
190
|
+
const overlay = React.createElement('div', { 'data-edrv-view': '1' },
|
|
191
|
+
React.createElement('div', { className: 'edrv-palette-mask', onClick: () => closeCommandPalette() }),
|
|
192
|
+
panel)
|
|
193
|
+
return createPortal(overlay, document.body)
|
|
194
|
+
}
|
|
@@ -11,6 +11,7 @@ import { summarize } from '../state/records.js'
|
|
|
11
11
|
import { nextDiffPath } from '../diffDock.js'
|
|
12
12
|
import { readDiffDock, subscribeDiffDock } from '../diffDockStore.js'
|
|
13
13
|
import { DiffBox } from './DiffBox.js'
|
|
14
|
+
import { CommandPalette } from './CommandPalette.js'
|
|
14
15
|
|
|
15
16
|
const nextIndexBySession = new Map()
|
|
16
17
|
|
|
@@ -60,12 +61,17 @@ export function ConversationDiffDock(props) {
|
|
|
60
61
|
}, content)
|
|
61
62
|
|
|
62
63
|
const editorSnapshot = readDiffDock(sessionId)
|
|
64
|
+
// 命令栏浮层的兜底宿主:本 dock 是每会话常驻 slot(编辑区未挂载时仍能唤起命令栏);
|
|
65
|
+
// 与 EditorView 同时挂载时由 commandPaletteStore 的单实例认领保证只渲染一份。
|
|
66
|
+
const palette = React.createElement(CommandPalette, { key: 'edrv-palette', sessionId })
|
|
63
67
|
if (editorSnapshot) {
|
|
64
|
-
if (editorSnapshot.mode === 'editor-empty' && !editorSnapshot.fileTotal) return
|
|
65
|
-
return
|
|
68
|
+
if (editorSnapshot.mode === 'editor-empty' && !editorSnapshot.fileTotal) return palette
|
|
69
|
+
return React.createElement(React.Fragment, null,
|
|
70
|
+
renderDock(React.createElement(DiffBox, Object.assign({}, editorSnapshot, { dock: true })), sessionId),
|
|
71
|
+
palette)
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
if (!sessionId || !summary?.pendingFiles?.length) return
|
|
74
|
+
if (!sessionId || !summary?.pendingFiles?.length) return palette
|
|
69
75
|
|
|
70
76
|
const paths = summary.pendingFiles.map((file) => file.path)
|
|
71
77
|
const click = () => {
|
|
@@ -75,10 +81,12 @@ export function ConversationDiffDock(props) {
|
|
|
75
81
|
if (next.path) openDiffView(next.path)
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
return React.createElement(React.Fragment, null,
|
|
85
|
+
renderDock(React.createElement(DiffBox, {
|
|
86
|
+
mode: 'chat',
|
|
87
|
+
dock: true,
|
|
88
|
+
fileTotal: paths.length,
|
|
89
|
+
onOpenNextFile: click,
|
|
90
|
+
}), sessionId || 'chat'),
|
|
91
|
+
palette)
|
|
84
92
|
}
|